Skip to content
This repository has been archived by the owner on Nov 10, 2022. It is now read-only.

Latest commit

 

History

History
602 lines (461 loc) · 23.2 KB

DEVELOPER.md

File metadata and controls

602 lines (461 loc) · 23.2 KB

Developer Notes

Supported Platforms

This code should work equivalently on MacOS, Linux, and Windows. However, it only works on Python 3.10 and earlier, because the asynctest library depends on @asyncio.coroutine, which was removed in Python 3.11.

Packaging and Dependencies

This project uses Poetry to manage Python packaging and dependencies. Most day-to-day tasks (such as running unit tests from the command line) are orchestrated through Poetry.

A coding standard is enforced using Black, isort and Pylint. Python 3 type hinting is validated using MyPy. To reduce boilerplate, classes are defined using Attrs (see this rationale).

To add dependencies use poetry add package (for runtime dependencies) or poetry add --group dev package (for development environment dependencies).

To update dependencies, use poetry update. This will update all of the dependencies without taking you past any major version changes that are likely to be incompatible. If you want to update a single package, use poetry update package. If you want to update past a major version change, either specify the version like poetry add package=^2.0.3 or get the latest with poetry add package@latest. PEP 508 specifications are also supported.

Pre-Commit Hooks

I rely on pre-commit hooks to ensure that the code is properly-formatted, clean, and type-safe when it's checked in. The run install step described below installs the project pre-commit hooks into your repository. These hooks are configured in .pre-commit-config.yaml.

If necessary, you can temporarily disable a hook using Git's --no-verify switch. However, keep in mind that the CI build on GitHub enforces these checks, so the build will fail.

Line Endings

The .gitattributes file controls line endings for the files in this repository. It would be simplest to have files in the Git working copy use native line endings. However, I develop this code on multiple platforms, and the files in the published PyPI package get the line endings from the working copy. If we use native line endings, the format of the published package will vary depending on where the publish step was run. This is confusing, and can cause problems for downstream users who expect the PyPI package to have a consistent format. Instead of relying on automatic behavior, the .gitattributes file forces most files to have UNIX line endings.

This generally works ok, except for the docs/requirements.txt file generated by Poetry. Unfortunately, all of the files that Poetry generates have native platform line endings, and you can't override that behavior. Even with sane configuration in .gitattributes, you sometimes still get spurious differences, where Git says that a file has changed but then git diff shows an empty result. The run script and the pre-commit hooks both normalize the line endings for requirements.txt. I wish there were a standard way to do this in Poetry or in Python, but there isn't as of this writing.

Prerequisites

Nearly all prerequisites are managed by Poetry. All you need to do is make sure that you have a working Python 3 enviroment and install Poetry itself.

Poetry Version

The project is designed to work with Poetry >= 1.2.0. If you already have an older version of Poetry installed on your system, uninstall it before following the setup process below:

curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | python3 - --uninstall

See the Announcing Poetry 1.2.0 blog post for more information.

MacOS

On MacOS, it's easiest to use Homebrew to install Python:

brew install python3

Once that's done, make sure the python on your $PATH is Python 3 from Homebrew (in /usr/local), rather than the standard Python 2 that comes with MacOS.

Although Poetry can also be installed from Homebrew, it works better to use to official installer:

curl -sSL https://install.python-poetry.org | python3 -

Note: The installer prints the location of the installed poetry script. Make sure to add this to your $PATH, otherwise you won't be able to run it.

Debian

First, install Python 3 and related tools:

sudo apt-get install python3 python3-venv python3-pip

Next, make sure that the python interpreter on your $PATH is Python 3.

If you are using Debian bullseye or later (first released in August 2021), then Python 2 is deprecated and Python 3 is the primary Python on your system. However, by default there is only a python3 interpreter on your $PATH, not a python interpreter. To add the python interpreter, use:

sudo apt-get install python-is-python3

For earlier releases of Debian where both Python 2 and Python 3 are available, the process is a little more complicated. The approach I used before upgrading to bullseye was based on update-alternatives, as discussed on StackExchange.

Next, install Poetry using the official installer:

curl -sSL https://install.python-poetry.org | python3 -

Note: The installer prints the location of the installed poetry script. Make sure to add this to your $PATH, otherwise you won't be able to run it.

Windows

First, install Python 3 from your preferred source, either a standard installer or a meta-installer like Chocolatey. Make sure the python on your $PATH is Python 3.

Next, install Poetry using the official installer:

curl -sSL https://install.python-poetry.org | python -

Note: The installer prints the location of the installed poetry script. Make sure to add this to your $PATH, otherwise you won't be able to run it.

The development environment (the run script, etc.) expects a bash shell to be available. On Windows, it works fine with the standard Git Bash.

Developer Tasks

The run script provides shortcuts for common developer tasks:

$ ./run --help

------------------------------------
Shortcuts for common developer tasks
------------------------------------

Basic tasks:

- run install: Setup the virtualenv via Poetry and install pre-commit hooks
- run format: Run the code formatters
- run checks: Run the code checkers
- run build: Build artifacts in the dist/ directory
- run test: Run the unit tests
- run test -c: Run the unit tests with coverage
- run test -ch: Run the unit tests with coverage and open the HTML report
- run suite: Run the complete test suite, as for the GitHub Actions CI build

Additional tasks:

- run demo: Run the demo client (requires running server)
- run docs: Build the Sphinx documentation for readthedocs.io
- run docs -o: Build the Sphinx documentation and open in a browser
- run publish: Publish the current code to PyPI and push to GitHub
- run release: Release a specific version and tag the code
- run requirements: Regenerate the docs/requirements.txt file
- run server: Start the websockets server

Running the Server

To run the server from the codebase for local testing, use the run server command. This is equivalent to the installed apologies-server script.

$ ./run server --help
usage: apologies-server [-h] [--quiet] [--verbose] [--debug] [--config CONFIG]
              [--logfile LOGFILE] [--override OVERRIDE]

Start the apologies server and let it run forever.

optional arguments:
  -h, --help           show this help message and exit
  --quiet              decrease log verbosity from INFO to ERROR
  --verbose            increase log verbosity from INFO to DEBUG
  --debug              like --verbose but also include websockets logs
  --config CONFIG      path to configuration on disk
  --logfile LOGFILE    path to logfile on disk (default is stdout)
  --override OVERRIDE  override a config parameter as "param:value"

By default, the server writes logs to stdout. If you prefer, you can specify
the path to a logfile, and logs will be written there instead. The default
configuration file is "/Users/kpronovici/.apologiesrc". If the default
configuration file is not found, default values will be set. If you override
the default config file, it must exist. You may override any individual config
parameter with "--override param:value".

The simplest way to start the server is with no arguments:

$ ./run server
2020-06-10 14:31:39,831Z --> [INFO   ] Apologies server started
2020-06-10 14:31:39,832Z --> [INFO   ] Configuration: {
  "logfile_path": null,
  "server_host": "localhost",
  "server_port": 8080,
  "close_timeout_sec": 10,
  "websocket_limit": 1000,
  "total_game_limit": 1000,
  "in_progress_game_limit": 25,
  "registered_player_limit": 100,
  "websocket_idle_thresh_min": 2,
  "websocket_inactive_thresh_min": 5,
  "player_idle_thresh_min": 15,
  "player_inactive_thresh_min": 30,
  "game_idle_thresh_min": 10,
  "game_inactive_thresh_min": 20,
  "game_retention_thresh_min": 2880,
  "idle_websocket_check_period_sec": 120,
  "idle_websocket_check_delay_sec": 300,
  "idle_player_check_period_sec": 120,
  "idle_player_check_delay_sec": 300,
  "idle_game_check_period_sec": 120,
  "idle_game_check_delay_sec": 300,
  "obsolete_game_check_period_sec": 300,
  "obsolete_game_check_delay_sec": 300
}
2020-06-10 14:31:39,832Z --> [INFO   ] Adding signal handlers...
2020-06-10 14:31:39,832Z --> [INFO   ] Scheduling tasks...
2020-06-10 14:31:39,832Z --> [INFO   ] Completed starting websocket server

The server displays its configuration when it boots. You can override any of this configuration using the switches on the run server command.

Running the Demo

While this is primarily a websockets server, it includes a quick'n'dirty demo that plays a game as a websockets client, to demonstrate the protocol and the websockets client code.

$ ./run demo --help
usage: demo [-h] [--quiet] [--verbose] [--debug] [--logfile LOGFILE]
            [--host HOST] [--port PORT]

Start the apologies server demo client.

optional arguments:
  -h, --help         show this help message and exit
  --quiet            decrease log verbosity from INFO to ERROR
  --verbose          increase log verbosity from INFO to DEBUG
  --debug            like --verbose but also include websockets logs
  --logfile LOGFILE  path to logfile on disk (default is stdout)
  --host HOST        host where the server is running
  --port PORT        port where the server is running on the host

The client requires that you already have the server running. By default, the
client writes logs to stdout. If you prefer, you can specify the path to a
logfile, and logs will be written there instead.

To run the demo, you must also have a server running elsewhere. Your simplest option is to start the server in one window:

./run server

and the demo in another window:

./run demo

The demo registers a "human" player, starts a 4-player game (getting 3 programmatic players as opponents), and then runs through the entire game until it completes. To simulate "human" game play, each time it is the human player's turn, a move is chosen randomly. The programmatic opponents are played as usual by the server using its reward-based play choice algorithm.

The demo assumes it can register a player with handle leela. You should be able to run the demo multiple times in a row against the server without any problems, because it cleans up after itself properly. However, if you have been making changes to the demo - or if it crashed or was interrupted - and the leela handle is still registered, the demo will fail to run.

Integration with PyCharm

Currently, I use PyCharm Community Edition as my day-to-day IDE. By integrating Black and Pylint, most everything important that can be done from a shell environment can also be done right in PyCharm.

PyCharm offers a good developer experience. However, the underlying configuration on disk mixes together project policy (i.e. preferences about which test runner to use) with system-specific settings (such as the name and version of the active Python interpreter). This makes it impossible to commit complete PyCharm configuration to the Git repository. Instead, the repository contains partial configuration, and there are instructions below about how to manually configure the remaining items.

Prerequisites

Before going any further, make sure sure that you have installed all of the system prerequisites discussed above. Then, make sure your environment is in working order. In particular, if you do not run the install step, there will be no virtualenv for PyCharm to use:

./run install && ./run suite

Open the Project

Once you have a working shell development environment, Open (do not Import) the apologies-server directory in PyCharm, then follow the remaining instructions below. By using Open, the existing .idea directory will be retained and all of the existing settings will be used.

Interpreter

As a security precaution, PyCharm does not trust any virtual environment installed within the repository, such as the Poetry .venv directory. In the status bar on the bottom right, PyCharm will report No interpreter. Click on this error and select Add Interpreter. In the resulting dialog, click Ok to accept the selected environment, which should be the Poetry virtual environment.

Project Structure

Go to the PyCharm settings and find the apologies-server project. Under Project Structure, mark both src and tests as source folders. In the Exclude Files box, enter the following:

LICENSE;NOTICE;PyPI.md;.coverage;.coveragerc;.github;.gitignore;.gitattributes;.htmlcov;.idea;.isort.cfg;.mypy.ini;.mypy_cache;.pre-commit-config.yaml;.pylintrc;.pytest_cache;.readthedocs.yml;.tabignore;build;dist;docs/_build;out;poetry.lock;poetry.toml;run;.run;.venv

When you're done, click Ok. Then, go to the gear icon in the project panel and uncheck Show Excluded Files. This will hide the files and directories in the list above.

Tool Preferences

In the PyCharm settings, go to Editor > Inspections and be sure that the Project Default profile is selected.

Unit tests are written using Pytest, and API documentation is written using Google Style Python Docstring. However, neither of these is the default in PyCharm. In the PyCharm settings, go to Tools > Python Integrated Tools. Under Testing > Default test runner, select pytest. Under Docstrings > Docstring format, select Google.

Running Unit Tests

Right click on the tests folder in the project explorer and choose Run 'pytest in tests'. Make sure that all of the tests pass. If you see a slightly different option (i.e. for "Unittest" instead of "pytest") then you probably skipped the preferences setup discussed above. You may need to remove the run configuration before PyCharm will find the right test suite.

External Tools

Optionally, you might want to set up external tools for some of common developer tasks: code reformatting and the PyLint and MyPy checks. One nice advantage of doing this is that you can configure an output filter, which makes the Pylint and MyPy errors clickable. To set up external tools, go to PyCharm settings and find Tools > External Tools. Add the tools as described below.

Linux or MacOS

On Linux or MacOS, you can set up the external tools to invoke the run script directly.

Shell Environment

For this to work, it's important that tools like poetry are on the system path used by PyCharm. On Linux, depending on how you start PyCharm, your normal shell environment may or may not be inherited. For instance, I had to adjust the target of my LXDE desktop shortcut to be the script below, which sources my profile before running the pycharm.sh shell script:

#!/bin/bash
source ~/.bash_profile
/opt/local/lib/pycharm/pycharm-community-2020.3.2/bin/pycharm.sh
Format Code
Field Value
Name Format Code
Description Run the code formatters
Group Developer Tools
Program $ProjectFileDir$/run
Arguments format
Working directory $ProjectFileDir$
Synchronize files after execution Checked
Open console for tool outout Checked
Make console active on message in stdout Unchecked
Make console active on message in stderr Unchecked
Output filters Empty
Run MyPy Checks
Field Value
Name Run MyPy Checks
Description Run the MyPy code checks
Group Developer Tools
Program $ProjectFileDir$/run
Arguments mypy
Working directory $ProjectFileDir$
Synchronize files after execution Unchecked
Open console for tool outout Checked
Make console active on message in stdout Checked
Make console active on message in stderr Checked
Output filters $FILE_PATH$:$LINE$:$COLUMN$:.*
Run Pylint Checks
Field Value
Name Run Pylint Checks
Description Run the Pylint code checks
Group Developer Tools
Program $ProjectFileDir$/run
Arguments pylint
Working directory $ProjectFileDir$
Synchronize files after execution Unchecked
Open console for tool outout Checked
Make console active on message in stdout Checked
Make console active on message in stderr Checked
Output filters $FILE_PATH$:$LINE$:$COLUMN.*

Windows

On Windows, PyCharm has problems invoking the run script. The trick is to invoke the Bash interpreter and tell it to invoke the run script. The examples below assume that you have installed Git Bash in its standard location under C:\Program Files\Git. If it is somewhere else on your system, just change the path for bash.exe.

Format Code
Field Value
Name Format Code
Description Run the code formatters
Group Developer Tools
Program powershell.exe
Arguments `& 'C:\Program Files\Git\bin\bash.exe' "./run" format
Working directory $ProjectFileDir$
Synchronize files after execution Checked
Open console for tool outout Checked
Make console active on message in stdout Unchecked
Make console active on message in stderr Unchecked
Output filters Empty
Run MyPy Checks
Field Value
Name Run MyPy Checks
Description Run the MyPy code checks
Group Developer Tools
Program powershell.exe
Arguments `& 'C:\Program Files\Git\bin\bash.exe' "./run" mypy
Working directory $ProjectFileDir$
Synchronize files after execution Unchecked
Open console for tool outout Checked
Make console active on message in stdout Checked
Make console active on message in stderr Checked
Output filters $FILE_PATH$:$LINE$:$COLUMN$:.*
Run Pylint Checks
Field Value
Name Run Pylint Checks
Description Run the Pylint code checks
Group Developer Tools
Program powershell.exe
Arguments `& 'C:\Program Files\Git\bin\bash.exe' "./run" pylint
Working directory $ProjectFileDir$
Synchronize files after execution Unchecked
Open console for tool outout Checked
Make console active on message in stdout Checked
Make console active on message in stderr Checked
Output filters $FILE_PATH$:$LINE$:$COLUMN.*

Release Process

Documentation

Documentation at Read the Docs is generated via a GitHub hook each time code is pushed to master. So, there is no formal release process for the documentation.

Code

Code is released to PyPI. There is a partially-automated process to publish a new release.

Note: In order to publish code, you must must have push permissions to the GitHub repo and be a collaborator on the PyPI project. Before running this process for the first time, you must set up a PyPI API token and configure Poetry to use it. (See notes below.)

Ensure that you are on the master branch. Releases must always be done from master.

Ensure that the Changelog is up-to-date and reflects all of the changes that will be published. The top line must show your version as unreleased:

Version 0.1.29     unreleased

Run the release step:

./run release 0.1.29

This updates pyproject.toml and the Changelog to reflect the released version, then commits those changes and tags the code. Nothing has been pushed or published yet, so you can always remove the tag (i.e. git tag -d v0.1.29) and revert your commit (git reset HEAD~1) if you made a mistake.

Finally, publish the release:

./run publish

This builds the deployment artifacts, publishes the artifacts to PyPI, and pushes the repo to GitHub. The code will be available on PyPI for others to use after a little while.

Configuring the PyPI API Token

In order to publish to PyPI, you must configure Poetry to use a PyPI API token. Once you have the token, you will configure Poetry to use it. Poetry relies on the Python keyring to store this secret. On MacOS and Windows, it will use the system keyring, and no other setup is required. If you are using Debian, the process is more complicated. See the notes below.

First, in your PyPI account settings, create an API token with upload permissions for the apologiesserver project. Once you have a working keyring, configure Poetry following the instructions:

poetry config pypi-token.pypi <the PyPI token>

Note that this leaves your actual secret in the command-line history, so make sure to scrub it once you're done.

Python Keyring on Debian

On Debian, the process really only works from an X session. There is a way to manipulate the keyring without being in an X session, and I used to document it here. However, it's so ugly that I don't want to encourage anyone to use it. If you want to dig in on your own, see the keyring documentation under the section Using Keyring on headless Linux systems.

Some setup is required to initialize the keyring in your Debian system. First, install the gnome-keyring package, and then log out:

$ sudo apt-get install gnome-keyring
$ exit

Log back in and initialize your keyring by setting and then removing a dummy value:

$ keyring set testvalue "user"
Password for 'user' in 'testvalue':
Please enter password for encrypted keyring:

$ keyring get testvalue "user"
Please enter password for encrypted keyring:
password

$ keyring del testvalue "user"
Deleting password for 'user' in 'testvalue':

At this point, the keyring should be fully functional and it should be ready for use with Poetry. Whenever Poetry needs to read a secret from the keyring, you'll get a popup window where you need to enter the keyring password.