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$variableassignments 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.defineis 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
<?phpopening tag, ending in the file's own line ending. A file with no<?phpfollowed by a newline cannot have one inserted, and raisesConfigWriteError. - 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()orset_variable()must be a PHP identifier ([A-Za-z_][A-Za-z0-9_]*); anything else raisesConfigKeyErrorbefore the content is touched. nan,infand-infhave no PHP literal and raiseConfigValueError. Written asrepr()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,intorfloat; - an
EnvLookup(name, default, function), for exactlygetenv_docker('NAME', <literal>)orgetenv('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_dockerreads the file named byNAME_FILE(Docker secrets) when that is set, thenNAMEeven if it is empty, then the default;?:falls back on''and'0'too;- a bare
getenvgivesFalsewhen 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)
| 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)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| 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 logRelease 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 logSecurity 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.
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 logA 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.