Skip to main content

l3io-wordpress-config

Read and write define() constants and $variable assignments in a WordPress wp-config.php file.

from l3io.wp.config import MISSING, WpConfigFile

config = WpConfigFile("/www/wp-config.php")

config.get("DB_NAME")                  # 'my_blog'
config.get("WP_DEBUG")                 # False  -- present and falsy
config.get("NOT_THERE") is MISSING     # True   -- absent
config.get_variable("table_prefix")    # 'wp_'  -- a $variable, not a define()

config.set("DB_NAME", "my_blog")       # written, read back, and verified

What this is, and what it is not

This is a reader and writer for the define() subset of PHP that WordPress itself writes, plus simple file-scope $variable assignments. It is not a PHP parser and must not be relied on as one.

Its scope is deliberately bounded, and the boundaries are these:

  • Only single-quoted string values are parsed as strings. define('X', "y") returns the raw text "y", quotes included.
  • A value must sit on one line. A statement spanning lines is not matched.
  • define() statements must start at the beginning of a line (leading whitespace aside), and $variable assignments at column 0, which approximates file scope: one indented inside a function body is not read. This is what makes a commented-out // define('X', 'y'); correctly invisible.
  • Anything inside a /* ... */ block comment is not read and never written, as PHP never runs it. define is matched in any case (DEFINE(...) is the same call to PHP). Heredoc and nowdoc strings are not recognised.
  • Quoted strings are read whole, so a ), ; or quote inside one never ends the value. WordPress's own salts contain ); in about one site in thirteen.
  • A value that is a PHP expression or references another constant is returned by get() as its raw source text, not evaluated. get_value() says which it is instead; see below.
  • Adding a constant inserts after the first <?php opening tag, ending in the file's own line ending. A file with no <?php followed by a newline cannot have one inserted, and raises ConfigWriteError.
  • Line endings and a UTF-8 BOM are kept as they are: WordPress ships its own sample with CRLF, and a write changes the one value, not every line.
  • A key written by set() or set_variable() must be a PHP identifier ([A-Za-z_][A-Za-z0-9_]*); anything else raises ConfigKeyError before the content is touched.
  • nan, inf and -inf have no PHP literal and raise ConfigValueError. Written as repr() gives them, PHP 8 would read them as undefined constants and fail on every page load — and read-back cannot catch that, because they round-trip.

The tests read and write a corpus of real configs (WordPress's own sample, the official Docker image's, multisite, common custom options, commented-out blocks, and a reverse-proxy setup), and PHP 8.4 grades them: what the package reads and writes is checked against what PHP itself runs, including 300 random writes.

Because those limits are real, every write is verified. WpConfigFile.set() writes to a temporary file, reads it back, and checks three things before committing: the constant written carries the value intended, every constant not written is byte-identical, and no key appears twice. On any mismatch it raises ReadBackError and the original file is left untouched.

The write is atomic and keeps who can read the file, which holds DB_PASSWORD. The temporary file has an unpredictable name and is opened exclusively, so nothing already in the site directory is written through. It takes the original's mode, owner and group before any content reaches it, and it is synced to disk before it is renamed over the original, as is the directory after. If the owner cannot be kept — a writer that is neither root nor the file's owner — the write raises ConfigWriteError and changes nothing, rather than handing the file to the writer. WpConfigFile never creates a wp-config.php: a file removed since it was read raises ConfigWriteError.

Replacing the whole file

WpConfigFile.replace_content(text) writes a complete wp-config.php through that same atomic, verified write: for example, a restored site's own config with this site's credentials already set in it. The text lands exactly as given, line endings included; mode and owner are kept; and the text is refused with ReadBackError if it defines any key twice. It returns False without rewriting when the file already holds that text.

from l3io.wp.config import WpConfigFile, WpConfigString

archived = WpConfigString(archived_text)        # the config being restored
target = WpConfigFile("/www/wp-config.php")     # the live one
archived.set("DB_PASSWORD", new_password)       # change what needs changing
target.replace_content(archived.content)

Copying a value from one config into another with get() and set() is only safe for a literal. get() returns an expression as its source text, and set() writes a str as a quoted string, so the official Docker image's define( 'DB_NAME', getenv_docker('WORDPRESS_DB_NAME', 'wordpress') ); copied that way becomes a database literally named getenv_docker('WORDPRESS_DB_NAME', 'wordpress').

Literals, environment lookups and expressions

get() returns an expression as its source text, a str indistinguishable from a quoted literal. The official wordpress Docker image writes every DB_* that way: define( 'DB_HOST', getenv_docker('WORDPRESS_DB_HOST', 'mysql') );. Use get_value() (and get_variable_value()) wherever the difference matters. It returns one of:

  • the literal's PHP value: str, bool, int or float;
  • an EnvLookup(name, default, function), for exactly getenv_docker('NAME', <literal>) or getenv('NAME'), optionally with ?: <literal>;
  • an Expression(source), for anything else, including octal and hex numbers, null, constants and concatenation;
  • MISSING.

EnvLookup.resolve(environ, read_file=None) gives the value PHP would define, so a caller never re-implements PHP's rules. Those rules differ by shape:

  • getenv_docker reads the file named by NAME_FILE (Docker secrets) when that is set, then NAME even if it is empty, then the default;
  • ?: falls back on '' and '0' too;
  • a bare getenv gives False when the variable is unset.

read_file is needed only when a NAME_FILE is set:

import os
from pathlib import Path
from l3io.wp.config import EnvLookup, Expression, WpConfigFile

value = WpConfigFile("/var/www/html/wp-config.php").get_value("DB_HOST")
if isinstance(value, EnvLookup):
    value = value.resolve(os.environ, lambda p: Path(p).read_text(encoding="utf-8"))
elif isinstance(value, Expression):
    raise SystemExit(f"DB_HOST is PHP code this cannot evaluate: {value.source}")

get_source() and get_variable_source() return a value exactly as written, and set_source(key, source) and set_variable_source(key, source) write one verbatim, through the same verified write as set(). So an environment lookup can be carried from one config into another as a lookup, not turned into a string:

archived.set_source("DB_HOST", target.get_source("DB_HOST"))

set_source() refuses, with ConfigValueError, any source that could end the statement, open a comment, leave PHP or unbalance a bracket, and any that would not read back as written. That check is structural, not a PHP parse: text that is balanced but is not a valid expression would be written. Pass source that came from a working config, which is what get_source() returns.

declares_function(name, before=None) says whether the config declares a function, such as the Docker image's getenv_docker(), which a carried getenv_docker(...) value needs. It finds function name( and function &name( in any case, outside strings and comments, and before=KEY also requires it to come before that key's live define. It is lexical, not a parse. It reports a declaration PHP never makes (in a dead branch, a class, or another function) as found. It does not follow include/require, and before= is text order, not PHP's hoisting of top-level functions. Against PHP's own function_exists() it agrees on every shape outside those limits. PHP 8.4 grades all of this in the test suite: every literal reading, and 200 random lookups across every environment state.

Install

pip install l3io-wordpress-config

Requires Python 3.11 or newer. It has no runtime dependencies, permanently.

Absent is not falsy

get() and get_variable() return the MISSING sentinel for a key that is not defined. A key defined as false, 0 or '' is present and falsy. Both are falsy in a boolean test, so compare identity:

if config.get("WP_DEBUG") is MISSING:
    ...   # not defined at all

Values keep the type of the PHP literal: quoted stays str (so '3306' is the string "3306", not a number), unquoted true/false become bool, unquoted integers become int, unquoted decimals become float.

Errors

Every failure is a package-owned exception under WpConfigError; no OSError, TypeError or ValueError crosses the public boundary.

Exception Raised when
ConfigReadError the file cannot be read
ConfigWriteError the file cannot be written, or a constant cannot be placed
ConfigKeyError a key to write is not a PHP identifier
ConfigValueError a value cannot be rendered as a PHP literal
ReadBackError verification failed; the file is unchanged

Command line

l3io-wordpress-config --filename /www/wp-config.php --key DB_NAME --value my_blog
l3io-wordpress-config --filename /www/wp-config.php --key DB_NAME

Also runnable as python -m l3io.wp.config. Exit codes are part of the interface: 0 success, 1 the key is not defined, 2 the operation failed. A key defined as false, 0 or '' prints its value and exits 0 — that is what distinguishes it from a key that is absent.

Flags: --filename, --key, --value, --set-true, --set-false, --log-level.

Development

uv sync --locked
make check      # lint, types, architecture contracts, tests
make ci         # replay the whole CI pipeline locally with nektos/act

uv is pinned by [tool.uv] required-version so the lockfile and CI cannot drift apart. Do not run uv self update without changing that pin and CI together.

Credits

Originally derived from cariad/py-wpconfigr by Cariad Eccleston (MIT; see LICENSE), whose maintained package is now wpconfigger. Earlier history is kept on the legacy/* branches and is not maintained.

Releases

Release notes are built from the commit log at release time and published on the GitHub releases page — there is no changelog file to keep in sync, and this section deliberately does not duplicate one.

Metadata

Release files for l3io-wordpress-config 0.1.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for l3io-wordpress-config 0.1.7
File Size Uploaded Provenance Actions
l3io_wordpress_config-0.1.7.tar.gz 157.0 kB With provenance Details for l3io_wordpress_config-0.1.7.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for l3io-wordpress-config 0.1.7
File Interpreter ABI Platform Actions
l3io_wordpress_config-0.1.7-py3-none-any.whl Python 3 none any Details for l3io_wordpress_config-0.1.7-py3-none-any.whl

Total release size: 186.1 kB

Release files / l3io_wordpress_config-0.1.7.tar.gz

Download URL l3io_wordpress_config-0.1.7.tar.gz
Size 157.0 kB
Tags Source
SHA-256 checksum
How to use checksums
4665f49fb82acabcee3adec1a6e0d4d336b9bca987ef979e6fca0ba522996844
BLAKE2b-256 checksum
How to use checksums
8ebdbe0118f29f53e9666f7e767bbfc0a302d53a80ed3813964acc3c181919b9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

Release files / l3io_wordpress_config-0.1.7-py3-none-any.whl

Download URL l3io_wordpress_config-0.1.7-py3-none-any.whl
Size 29.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8f94dc06f51ffbe1ea57f327585191883dec918484e61995772662fe03476ce7
BLAKE2b-256 checksum
How to use checksums
186011cc9ea9a60b47f31ab24801baf350dd3ab7c9d1b3e4cfb04a09745b4e04
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

Security for l3io-wordpress-config 0.1.7

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for l3io_wordpress_config-0.1.7.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 10, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.7 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page