Skip to content
Back to top
View this page

# Contributing

We are happy you have decided to contribute to Twine.

Please see the GitHub repository for code and more documentation, and the official Python Packaging User Guide for user documentation. To ask questions or get involved, you can join the Python Packaging Discourse forum, `#pypa` or `#pypa-dev` on IRC, or the distutils-sig mailing list.

Everyone interacting in the Twine project’s codebases, issue trackers, chat rooms, and mailing lists is expected to follow the PSF Code of Conduct.

## Getting started

We use tox to run tests, check code style, and build the documentation. To install `tox`, run:

    python3 -m pip install tox

Clone the twine repository from GitHub, then run:

    cd /path/to/your/local/twine
    tox -e dev

This creates a virtual environment, so that twine and its dependencies do not interfere with other packages installed on your machine. In the virtual environment, `twine` is pointing at your local copy, so when you make changes, you can easily see their effect.

The virtual environment also contains the tools for running tests and checking code style, so you can run them on single files directly or in your code editor. However, we still encourage using the `tox` commands below on the whole codebase.

To use the virtual environment, run:

    source venv/bin/activate

### Building the documentation

Additions and edits to twine’s documentation are welcome and appreciated.

To preview the docs while you’re making changes, run:

    tox -e watch-docs

Then open a web browser to http://127.0.0.1:8000.

When you’re done making changes, lint and build the docs locally before making a pull request. In your active virtual environment, run:

    tox -e docs

The HTML of the docs will be written to `docs/_build/html`.

### Code style

To automatically reformat your changes with isort and black, run:

    tox -e format

To detect any remaining code smells with flake8, run:

    tox -e lint

To perform strict type-checking using mypy, run:

    tox -e types

Any errors from `lint` or `types` need to be fixed manually.

Additionally, we prefer that `import` statements be used for packages and modules only, rather than individual classes or functions.

### Testing

We use pytest for writing and running tests.

To run the tests in your virtual environment, run:

    tox -e py

To pass options to `pytest`, e.g. the name of a test, run:

    tox -e py -- tests/test_upload.py::test_exception_for_http_status

Twine is continuously tested against supported versions of Python using GitHub Actions. To run the tests against a specific version, e.g. Python 3.8, you will need it installed on your machine. Then, run:

    tox -e py38

To run the “integration” tests of uploading to real package indexes, run:

    tox -e integration

To run the tests against all supported Python versions, check code style, and build the documentation, run:

    tox

## Submitting changes

1. Fork the GitHub repository.

2. Make a branch off of `main` and commit your changes to it.

3. Run the tests, check code style, and build the docs as described above.

4. Optionally, add your name to the end of the `AUTHORS` file using the format `Name <email@domain.com> (url)`, where the `(url)` portion is optional.

5. Submit a pull request to the `main` branch on GitHub, referencing an open issue.

6. Add a changelog entry.

### Changelog entries

The `docs/changelog.rst` file is built by towncrier from files in the `changelog/` directory. To add an entry, create a file in that directory named `{number}.{type}.rst`, where `{number}` is the pull request number, and `{type}` is `feature`, `bugfix`, `doc`, `removal`, or `misc`.

For example, if your PR number is 1234 and it’s fixing a bug, then you would create `changelog/1234.bugfix.rst`. PRs can span multiple categories by creating multiple files: if you added a feature and deprecated/removed an old feature in PR #5678, you would create `changelog/5678.feature.rst` and `changelog/5678.removal.rst`.

A changelog entry is meant for end users and should only contain details relevant to them. In order to maintain a consistent style, please keep the entry to the point, in sentence case, shorter than 80 characters, and in an imperative tone. An entry should complete the sentence “This change will …”. If one line is not enough, use a summary line in an imperative tone, followed by a description of the change in one or more paragraphs, each wrapped at 80 characters and separated by blank lines.

You don’t need to reference the pull request or issue number in a changelog entry, since towncrier will add a link using the number in the file name, and the pull request should reference an issue number. Similarly, you don’t need to add your name to the entry, since that will be associated with the pull request.

Changelog entries are rendered using reStructuredText, but they should only have minimal formatting (such as ```monospaced text```).

## Architectural overview

Twine is a command-line tool for interacting with PyPI securely over HTTPS. Its three purposes are to be:

1. A user-facing tool for publishing on pypi.org

2. A user-facing tool for publishing on other Python package indexes (e.g., `devpi` instances)

3. A useful API for other programs (e.g., `zest.releaser`) to call for publishing on any Python package index

Currently, twine has two principal functions: uploading new packages and registering new projects (`register` is no longer supported on PyPI, and is in Twine for use with other package indexes).

Its command line arguments are parsed in `twine/cli.py`. The code for registering new projects is in `twine/commands/register.py`, and the code for uploading is in `twine/commands/upload.py`. The file `twine/package.py` contains a single class, `PackageFile`, which hashes the project files and extracts their metadata. The file `twine/repository.py` contains the `Repository` class, whose methods control the URL the package is uploaded to (which the user can specify either as a default, in the `.pypirc` file, or pass on the command line), and the methods that upload the package securely to a URL.

For more details, refer to the source documentation (currently a work in progress):

- twine package
  - twine.commands package
    - twine.commands.check module
      - `_WarningStream`
        - `_WarningStream.write()`
      - `_parse_content_type()`
      - `_check_file()`
      - `check()`
      - `main()`
    - twine.commands.register module
      - `register()`
      - `main()`
    - twine.commands.upload module
      - `skip_upload()`
      - `_make_package()`
      - `upload()`
      - `main()`
  - twine.auth module
    - `TOKEN_RENEWAL_THRESHOLD`
    - `CredentialInput`
      - `CredentialInput.__init__()`
    - `TrustedPublishingTokenRetrievalError`
      - `TrustedPublishingTokenRetrievalError.code`
      - `TrustedPublishingTokenRetrievalError.description`
    - `TrustedPublishingToken`
      - `TrustedPublishingToken.message`
      - `TrustedPublishingToken.errors`
      - `TrustedPublishingToken.token`
      - `TrustedPublishingToken.success`
      - `TrustedPublishingToken.expires`
    - `TrustedPublishingAuthenticator`
      - `TrustedPublishingAuthenticator.__init__()`
    - `Resolver`
      - `Resolver._tp_token`
      - `Resolver._expires`
      - `Resolver.__init__()`
      - `Resolver.authenticator`
      - `Resolver.choose()`
      - `Resolver.username`
      - `Resolver.password`
      - `Resolver._has_valid_cached_tp_token()`
      - `Resolver._make_trusted_publishing_token()`
      - `Resolver.make_trusted_publishing_token()`
      - `Resolver.system`
      - `Resolver.get_username_from_keyring()`
      - `Resolver.get_password_from_keyring()`
      - `Resolver.username_from_keyring_or_prompt()`
      - `Resolver.password_from_keyring_or_trusted_publishing_or_prompt()`
      - `Resolver.prompt()`
      - `Resolver.is_pypi()`
    - `Private`
      - `Private.prompt()`
  - twine.cli module
    - `configure_output()`
    - `list_dependencies_and_versions()`
    - `dep_versions()`
    - `dispatch()`
  - twine.exceptions module
    - `TwineException`
    - `RedirectDetected`
      - `RedirectDetected.from_args()`
    - `PackageNotFound`
    - `UploadToDeprecatedPyPIDetected`
      - `UploadToDeprecatedPyPIDetected.from_args()`
    - `UnsupportedConfiguration`
      - `UnsupportedConfiguration.Builder`
        - `UnsupportedConfiguration.Builder.__init__()`
        - `UnsupportedConfiguration.Builder.repository_url`
        - `UnsupportedConfiguration.Builder.features`
        - `UnsupportedConfiguration.Builder.with_repository_url()`
        - `UnsupportedConfiguration.Builder.with_feature()`
        - `UnsupportedConfiguration.Builder.finalize()`
    - `UnreachableRepositoryURLDetected`
    - `InvalidSigningConfiguration`
    - `InvalidSigningExecutable`
    - `InvalidConfiguration`
    - `InvalidDistribution`
    - `NonInteractive`
    - `TrustedPublishingFailure`
    - `InvalidPyPIUploadURL`
  - twine.package module
    - `_safe_name()`
    - `PackageMetadata`
      - `PackageMetadata.metadata_version`
      - `PackageMetadata.name`
      - `PackageMetadata.version`
      - `PackageMetadata.platform`
      - `PackageMetadata.summary`
      - `PackageMetadata.description`
      - `PackageMetadata.keywords`
      - `PackageMetadata.home_page`
      - `PackageMetadata.author`
      - `PackageMetadata.author_email`
      - `PackageMetadata.license`
      - `PackageMetadata.supported_platform`
      - `PackageMetadata.download_url`
      - `PackageMetadata.classifiers`
      - `PackageMetadata.requires`
      - `PackageMetadata.provides`
      - `PackageMetadata.obsoletes`
      - `PackageMetadata.maintainer`
      - `PackageMetadata.maintainer_email`
      - `PackageMetadata.requires_dist`
      - `PackageMetadata.provides_dist`
      - `PackageMetadata.obsoletes_dist`
      - `PackageMetadata.requires_python`
      - `PackageMetadata.requires_external`
      - `PackageMetadata.project_urls`
      - `PackageMetadata.description_content_type`
      - `PackageMetadata.provides_extra`
      - `PackageMetadata.dynamic`
      - `PackageMetadata.license_expression`
      - `PackageMetadata.license_file`
      - `PackageMetadata.comment`
      - `PackageMetadata.pyversion`
      - `PackageMetadata.filetype`
      - `PackageMetadata.gpg_signature`
      - `PackageMetadata.attestations`
      - `PackageMetadata.sha256_digest`
      - `PackageMetadata.blake2_256_digest`
    - `PackageFile`
      - `PackageFile.__init__()`
      - `PackageFile.version`
      - `PackageFile.gpg_signature`
      - `PackageFile.attestations`
      - `PackageFile.from_filename()`
      - `PackageFile.metadata_dictionary()`
      - `PackageFile.add_attestations()`
      - `PackageFile.add_gpg_signature()`
      - `PackageFile.sign()`
      - `PackageFile.run_gpg()`
    - `Hexdigest`
      - `Hexdigest.sha2`
      - `Hexdigest.blake2`
      - `Hexdigest.__new__()`
      - `Hexdigest._asdict()`
      - `Hexdigest._field_defaults`
      - `Hexdigest._fields`
      - `Hexdigest._make()`
      - `Hexdigest._replace()`
    - `HashManager`
      - `HashManager.__init__()`
      - `HashManager._sha2_update()`
      - `HashManager._sha2_hexdigest()`
      - `HashManager._blake_update()`
      - `HashManager._blake_hexdigest()`
      - `HashManager.hash()`
      - `HashManager.hexdigest()`
  - twine.repository module
    - `Repository`
      - `Repository.__init__()`
      - `Repository.close()`
      - `Repository._convert_metadata_to_list_of_tuples()`
      - `Repository.set_certificate_authority()`
      - `Repository.set_client_certificate()`
      - `Repository.register()`
      - `Repository._upload()`
      - `Repository.upload()`
      - `Repository.package_is_uploaded()`
      - `Repository.release_urls()`
      - `Repository.verify_package_integrity()`
  - twine.settings module
    - `Settings`
      - `Settings.__init__()`
      - `Settings.username`
      - `Settings.password`
      - `Settings._allow_noninteractive()`
      - `Settings.verbose`
      - `Settings.register_argparse_arguments()`
      - `Settings.from_argparse()`
      - `Settings._handle_package_signing()`
      - `Settings._handle_repository_options()`
      - `Settings._handle_certificates()`
      - `Settings.verify_feature_capability()`
      - `Settings.check_repository_url()`
      - `Settings.create_repository()`
  - twine.utils module
    - `get_config()`
    - `sanitize_url()`
    - `_validate_repository_url()`
    - `get_repository_from_config()`
    - `_config_from_repository_url()`
    - `normalize_repository_url()`
    - `get_file_size()`
    - `check_status_code()`
    - `get_userpass_value()`
    - `get_cacert()`
    - `get_clientcert()`
    - `make_requests_session()`
    - `EnvironmentDefault`
      - `EnvironmentDefault.__init__()`
    - `EnvironmentFlag`
      - `EnvironmentFlag.__init__()`
      - `EnvironmentFlag.bool_from_env()`
  - twine.wheel module
    - `Wheel`
      - `Wheel.__init__()`
      - `Wheel.py_version`
      - `Wheel.find_candidate_metadata_files()`
      - `Wheel.read()`

### Where Twine gets configuration and credentials

A user can set the repository URL, username, and/or password via command line, `.pypirc` files, environment variables, and `keyring`.

## Adding a maintainer

A checklist for adding a new maintainer to the project.

1. Add them as a Member in the GitHub repo settings.

2. Get them Test PyPI and canon PyPI usernames and add them as a Maintainer on our Test PyPI project and canon PyPI.

## Making a new release

A checklist for creating, testing, and distributing a new version.

1. Choose a version number, and create a new branch

       VERSION=3.4.2

       git switch -c release-$VERSION

2. Update `docs/changelog.rst`

       tox -e changelog -- --version $VERSION

       git commit -am "Update changelog for $VERSION"

3. Open a pull request for review

4. Merge the pull request, and ensure the GitHub Actions build passes

5. Create a new git tag for the version

       git switch main

       git pull --ff-only upstream main

       git tag -m "Release v$VERSION" $VERSION

6. Push to start the release, and watch it in GitHub Actions

       git push upstream $VERSION

7. View the new release on PyPI

## Future development

See our open issues.

In the future, `pip` and `twine` may merge into a single tool; see ongoing discussion.