Developer Instructions¶
Guidance for developers.
Pre-Commit Hooks¶
We use the excellent pre-commit to run several hooks on all changes before commits.
pre-commit is included in the dev extra installs. You’ll have to run pre-commit install once per environment
before committing changes.
The reason behind running ruff, and others as a pre-commit hook is to let a machine make style decisions, based on the collective wisdom of the Python community.
Generating Documentation¶
You will need to pip install the dev requirements:
pip install -e .[dev]
Then from the root of the repo you can type:
make sphinx
This will automatically regenerate the api documentation using sphinx-apidoc. The rendered documentation will be
stored in the /docs/build directory. The generated documentation is served from the gh-pages branch. Make sure
that the branch is clean and then to push to gh-pages you can type:
make ghpages
Note about documentation: The Numpy and Google style docstrings are activated by default. Just make sure Sphinx 1.3 or above is installed.
Run unit tests¶
Run python -m pytest to run all tests defined in the tests subfolder.
CI runs the suite twice, once with the default pytest.ini and once with
pytest-ansi.ini, which differs only by enabling spark.sql.ansi.enabled.
A change touching Spark casting or null handling should be run both ways:
python -m pytest
python -m pytest -c pytest-ansi.ini
The Spark tests need the spark extra and Java 17. Newer JDKs fail with
py4j.protocol errors. If the JDK came from conda, JAVA_HOME has to point
at it, which a non-interactive shell will not inherit:
export JAVA_HOME=$CONDA_PREFIX/lib/jvm
Local CI matrix with tox¶
Running the matrix by hand means installing the right JDK per Spark version,
switching between pytest.ini, pytest-ansi.ini and pytest-connect.ini,
and re-pinning pyspark[connect] for each track. tox.ini does that for
you: it mirrors .github/workflows/test-package.yml job for job, and pulls
Java and PySpark from conda-forge via tox-conda
so no system JDK is required.
Using tox is optional. The plain pytest and make commands above still
work and are what the individual test targets are for.
Install the tooling, then run an environment:
pip install "tox<4" tox-conda
tox -e lint # ruff check + ruff format --check
tox -e py312-nospark # no pyspark; spark tests skip
tox -e py312-spark4-pandas3 # Spark 4, ANSI mode, Spark Connect
tox -e typecheck # mypy (not run by CI)
tox # every env in envlist
Each environment maps to a CI job:
tox environment |
CI job |
What it covers |
|---|---|---|
|
|
|
|
|
pandas, polars, base, report and comparator tests; Spark and
Snowflake tests skip via |
|
|
Spark 3.5 classic session, plus a second pass in ANSI mode |
|
|
Spark 4 classic session |
|
|
Spark 4 classic session, ANSI mode, and both Spark Connect suites |
The Spark environments are the entire cost of a run, roughly twenty minutes
each, while the four no-Spark environments finish in under a minute. The
matrix therefore covers the Python, pandas and Spark axes independently
rather than as a cross product. Read the header of tox.ini before
widening it: it records which combinations are left out and what adding one
costs. Environments outside envlist still run on demand, so
tox -e py313-spark4-pandas3 works without being part of the default set.
Two constraints worth knowing before you edit tox.ini:
The Spark environments pin
openjdk>=17.0.8,<18. conda-forge ships GraalVM builds of openjdk 17 below that floor, and selecting one drags ingraalpy-graalvm, which replaces CPython with GraalPy and breaks pip. Do not relax the floor back to a bareopenjdk=17.A classic Spark session and a Spark Connect session cannot coexist in one pytest process: starting a local Connect server sets
SPARK_LOCAL_REMOTE, after which every laterSparkSession.builder.getOrCreate()returns the Connect session. That is why the Connect suites are separate commands, and whySPARK_LOCAL_REMOTEis deliberately absent frompassenv.
Snowflake testing¶
The Snowflake tests run either against a live Snowflake session or against Snowpark’s local testing mode:
python -m pytest tests/test_snowflake.py
python -m pytest tests/test_snowflake.py --snowflake-session local
Local testing mode is an emulator rather than Snowflake, and two of its
limitations matter here: eqNullSafe returns True for every row, and
high-precision decimals are truncated when a DataFrame is created. Tests that
depend on either request the requires_live_snowflake_session fixture, which
skips them in local mode. Changes to SnowflakeCompare still need a live
session to be fully validated, and that validation does not happen in CI.
A live session is built from the following environment variables, using external browser authentication rather than a password:
SF_ACCOUNT: your Snowflake accountSF_UID: your Snowflake usernameSF_WAREHOUSE: the warehouse to useSF_DATABASE: a database you have access toSF_SCHEMA: a schema belonging to that database
Management of Requirements¶
Requirements of the project should be added to pyproject.toml. Optional requirements used only for testing,
documentation, or code quality are added to pyproject.toml in the project.optional-dependencies section.
edgetest¶
edgetest is a utility to help keep requirements up to date and ensure a subset of testing requirements still work. More on edgetest here.
edgetest and tox do not interfere with each other. edgetest creates its
environments under .edgetest/ in the repository root and tox creates
its own under .tox/; both directories are gitignored, and neither tool
reads the other’s configuration. The edgetest extra and the tox
environments can be installed side by side.
The pyproject.toml has configuration details on how to run edgetest. The process is automated by the
edgetest GitHub Actions workflow, which opens a pull request with any dependency bumps it finds.
In order to execute edgetest locally you can run the following after install edgetest:
edgetest -c pyproject.toml --export
This should return output like the following and also updating pyproject.toml:
============= =============== =================== =================
Environment Passing tests Upgraded packages Package version
============= =============== =================== =================
core True boto3 1.21.7
core True pandas 1.3.5
core True PyYAML 6.0
============= =============== =================== =================
Release Guide¶
For datacompy we want to use a simple trunk-based workflow and follow
Semantic Versioning for each release.
main is the single active branch where all day-to-day development happens. All feature branches must be squash
merged into main. The reason we squash merge is to keep the branch history clean and prevent it from being
polluted with interim commit messages. Squashing collapses all the commits into one single new commit, which also
makes it easier to back out changes if something breaks.
Releases are cut directly from main by tagging the desired commit with the appropriate version. Each tag should
correspond to a published artifact on PyPI that users can pip install.
gh-pages is where official documentation will go. After each release you should build the docs and push the HTML to
the pages branch. When first setting up the repo you want to make sure your gh-pages is a orphaned branch since it is
disconnected and independent from the code: git checkout --orphan gh-pages.
The repo has a Makefile in the root folder which has helper commands such as make sphinx, and
make ghpages to help streamline building and pushing docs once they are setup right.
Generating distribution archives (PyPI)¶
After each release the package will need to be uploaded to PyPi. The instructions below are taken from packaging.python.org
Update / Install build, wheel, and twine:
pip install --upgrade build wheel twine
Generate distributions:
python -m build
Under the dist folder you should have something as follows:
dist/
datacompy-0.1.0-py3-none-any.whl
datacompy-0.1.0.tar.gz
Finally upload to PyPi:
# test pypi
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
# real pypi
twine upload dist/*