From f400c24cb5814427af39aaabcd86ca4853bc05c4 Mon Sep 17 00:00:00 2001 From: Tom Chen Date: Mon, 21 Dec 2020 22:19:00 +0100 Subject: [PATCH] init --- .editorconfig | 20 +++ .github/workflows/release.yml | 26 ++++ .github/workflows/test.yml | 26 ++++ .gitignore | 138 +++++++++++++++++++ .vscode/settings.json | 6 + LICENSE | 21 +++ MANIFEST.in | 4 + README.md | 246 ++++++++++++++++++++++++++++++++++ pyproject.toml | 3 + setup.cfg | 3 + setup.py | 53 ++++++++ src/examplepy/__init__.py | 3 + src/examplepy/module1.py | 22 +++ tests/__init__.py | 0 tests/test_module1.py | 13 ++ tox.ini | 13 ++ vscode.env | 1 + 17 files changed, 598 insertions(+) create mode 100644 .editorconfig create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 src/examplepy/__init__.py create mode 100644 src/examplepy/module1.py create mode 100644 tests/__init__.py create mode 100644 tests/test_module1.py create mode 100644 tox.ini create mode 100644 vscode.env diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9e71a42 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.py] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..91100b4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: Release + +on: + release: + types: [created] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python setup.py sdist bdist_wheel + twine upload --repository pypi dist/* diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a462161 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Test + +on: + push: + branches: + - main + - master + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.6, 3.7, 3.8, 3.9] + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install Tox and any other packages + run: pip install tox + - name: Run Tox + # Run tox using the version of Python in `PATH` + run: tox -e py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a81c8ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,138 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..81002b9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "python.testing.unittestEnabled": false, + "python.testing.nosetestsEnabled": false, + "python.testing.pytestEnabled": true, + "python.envFile": "${workspaceRoot}/vscode.env" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..373e60f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Tom Chen (tomchen.org) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b1dbe7e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include pyproject.toml +include *.md +include LICENSE +recursive-include tests test*.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7dbba2 --- /dev/null +++ b/README.md @@ -0,0 +1,246 @@ +# Example PyPI (Python Package Index) Package & Tutorial / Instruction / Workflow for 2021 + +This is an example [PyPI](https://pypi.org/) (Python Package Index) package set up with automated tests and package publishing using GitHub Actions CI/CD. It is made primarily for GitHub + VS Code (Windows / Mac / Linux) users who are about to write and publish their first PyPI package. The package could serve as a starter / boilerplate and the tutorial could give you a quick and concise explaination to solve some small but annoying problems you might encounter, such as package / module name confusion, and VS Code test configuration issues. + +
Differences from pypa/sampleproject (click to show/hide) + +This example package is inspired by / based on the [official sample project pypa/sampleproject](https://github.com/pypa/sampleproject), but this package: + +- is a simplified version of pypa/sampleproject (and the [official Python Packaging User Guide](https://packaging.python.org/)) +- uses GitHub Actions for both testing and publishing, instead of Travis CI +- is tested when pushing `master` or `main` branch, and is published when create a release +- includes test files in the source distribution +- uses **setup.cfg** for [version single-sourcing](https://packaging.python.org/guides/single-sourcing-package-version/) (setuptools 46.4.0+) +- has **.vscode\settings.json** and **vscode.env** which adds **src/** folder to `PYTHONPATH`, so that test files don't have linting errors and may run with pytest in VS Code +- does not use flake8 for automated linting - it is sometimes too strict and inflexible, you may use pylint locally instead +- has this tutorial that covers everything you need to know in one page. Everything that might not be very useful, is hidden in collapsible sections that you can click to show +- has **[.editorconfig](https://editorconfig.org/#download)** file + +
+ +## Make necessary changes + +Fork, clone or download the repository [github.com/tomchen/example_pypi_package](https://github.com/tomchen/example_pypi_package). + +### Package, module name + +Many use a same package and module name, you could definitely do that. But this example package and its module's names are different: `example_pypi_package` and `examplepy`. + +Open `example_pypi_package` folder with Visual Studio Code, Ctrl + Shift + F (Windows / Linux) or Cmd + Shift + F (MacOS) to find all occurrences of both names and replace them with your package and module's names. Also remember to change the name of the folder **src/examplepy**. + +Simply and very roughly speaking, package name is used in `pip install ` and module name is used in `import `. Both names should consist of lowercase basic letters (a-z). They may have underscores (`_`) if you really need them. Hyphen-minus (`-`) should not be used. + +You'll also need to make sure the URL "https://pypi.org/project/example-pypi-package/" (replace `example-pypi-package` by your package name, with all `_` becoming `-`) is not occupied. + +
Details on naming convention (click to show/hide) + +Underscores (`_`) can be used but such use is discouraged. Numbers can be used if the name does not start with a number, but such use is also discouraged. + +Name starting with a number and/or containing hyphen-minus (`-`) should not be used: although technically legal, such name causes a lot of trouble − users have to use `importlib` to import it. + +Don't be fooled by the URL "[pypi.org/project/example-pypi-package/](https://pypi.org/project/example-pypi-package/)" and the name "example-pypi-package" on pypi.org. pypi.org and pip system convert all `_` to `-` and use the latter on the website / in `pip` command, but the real name is still with `_`, which users should use when importing the package. + +There's also [namespace](https://packaging.python.org/guides/packaging-namespace-packages/) to use if you need sub-packages. + +
+ +### Other changes + +Make necessary changes in **setup.py**. + +The package's version number `__version__` is in **src/examplepy/\_\_init\_\_.py**. You may want to change that. + +The example package is designed to be compatible with Python 3.6, 3.7, 3.8, 3.9, and will be tested against these versions. If you need to change the version range, you should change: + +- `classifiers`, `python_requires` in **setup.py** +- `envlist` in **tox.ini** +- `matrix: python:` in **.github/workflows/test.yml** + +If you plan to upload to [TestPyPI](https://test.pypi.org/) which is a playground of [PyPI](https://pypi.org/) for testing purpose, change `twine upload --repository pypi dist/*` to `twine upload --repository testpypi dist/*` in the file **.github/workflows/release.yml**. + +## Development + +### pip + +pip is a Python package manager. You already have pip if you use Python 3.4 and later version which include it by default. Read [this](https://pip.pypa.io/en/stable/installing/#do-i-need-to-install-pip) to know how to check whether pip is installed. Read [this](https://pip.pypa.io/en/stable/installing/#installing-with-get-pip-py) if you need to install it. + +### Use VS Code + +Visual Studio Code is the most popular code editor today, our example package is configured to work with VS Code. + +Install VS Code extension "[Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python)". + +"Python" VS Code extension will suggest you install pylint. Also, the example package is configured to use pytest with VS Code + Python extensions, so, install pylint and pytest: + +```bash +pip install pylint pytest +``` + +(It's likely you will be prompted to install them, if that's the case, you don't need to type and execute the command) + +**vscode.env**'s content is now `PYTHONPATH=/;src/;${PYTHONPATH}` which is good for Windows. If you use Linux or MacOS, you need to change it to `PYTHONPATH=/:src/:${PYTHONPATH}` (replacing `;` with `:`). If the PATH is not properly set, you'll see linting errors in test files and pytest won't be able to run **tests/test\_\*.py** files correctly. + +Close and reopen VS Code. You can now click the lab flask icon in the left menu and run all tests there, with pytest. pytest seems better than the standard unittest framework, it supports `unittest` thus you can keep using `import unittest` in your test files. + +The example package also has a **.editorconfig** file. You may install VS Code extension "[EditorConfig for VS Code](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig)" that uses the file. With current configuration, the EditorConfig tool can automatically use spaces (4 spaces for .py, 2 for others) for indentation, set `UTF-8` encoding, `LF` end of lines, trim trailing whitespaces in non Markdown files, etc. + +In VS Code, you can go to File -> Preferences -> Settings, type "Python Formatting Provider" in the search box, and choose one of the three Python code formatting tools (autopep8, black and yapf), you'll be prompted to install it. The shortcuts for formatting of a code file are Shift + Alt + F (Windows); Shift + Option (Alt) + F (MacOS); Ctrl + Shift + I (Linux). + +### Write your package + +In **src/examplepy/** (`examplepy` should have been replaced by your module name) folder, rename **module1.py** and write your code in it. Add more module .py files if you need to. + +### Write your tests + +In **tests/** folder, rename **test_module1.py** (to **test\_\*.py**) and write your unit test code (with [unittest](https://docs.python.org/3/library/unittest.html)) in it. Add more **test\_\*.py** files if you need to. + +
The testing tool `tox` will be used in the automation with GitHub Actions CI/CD. If you want to use `tox` locally, click to read the "Use tox locally" section + +### Use tox locally + +Install tox and run it: + +```bash +pip install tox +tox +``` + +In our configuration, tox runs a check of source distribution using [check-manifest](https://pypi.org/project/check-manifest/) (which requires your repo to be git-initialized (`git init`) and added (`git add .`) at least), setuptools's check, and unit tests using pytest. You don't need to install check-manifest and pytest though, tox will install them in a separate environment. + +The automated tests are run against several Python versions, but on your machine, you might be using only one version of Python, if that is Python 3.9, then run: + +```bash +tox -e py39 +``` + +
+ +If you add more files to the root directory (**example_pypi_package/**), you'll need to add your file to `check-manifest --ignore` list in **tox.ini**. + +
Thanks to GitHub Actions' automated process, you don't need to generate distribution files locally. But if you insist, click to read the "Generate distribution files" section + +## Generate distribution files + +### Install tools + +Install or upgrade `setuptools` and `wheel`: + +```bash +python -m pip install --user --upgrade setuptools wheel +``` + +(If `python3` is the command on your machine, change `python` to `python3` in the above command, or add a line `alias python=python3` to **~/.bashrc** or **~/.bash_aliases** file if you use bash on Linux) + +### Generate `dist` + +From `example_pypi_package` directory, run the following command, in order to generate production version for source distribution (sdist) in `dist` folder: + +```bash +python setup.py sdist bdist_wheel +``` + +### Install locally + +Optionally, you can install dist version of your package locally before uploading to [PyPI](https://pypi.org/) or [TestPyPI](https://test.pypi.org/): + +```bash +pip install dist/example_pypi_package-0.1.0.tar.gz +``` + +(You may need to uninstall existing package first: + +```bash +pip uninstall example_pypi_package +``` + +There may be several installed packages with the same name, so run `pip uninstall` multiple times until it says no more package to remove.) + +
+ +## Upload to PyPI + +### Register on PyPI and get token + +Register an account on [PyPI](https://pypi.org/), go to [Account settings § API tokens](https://pypi.org/manage/account/#api-tokens), "Add API token". The PyPI token only appears once, copy it somewhere. If you missed it, delete the old and add a new token. + +(Register a [TestPyPI](https://test.pypi.org/) account if you are uploading to TestPyPI) + +### Set secret in GitHub repo + +On the page of your newly created or existing GitHub repo, click **Settings** -> **Secrets** -> **New repository secret**, the **Name** should be `PYPI_API_TOKEN` and the **Value** should be your PyPI token (which starts with `pypi-`). + +### Push or release + +The example package has automated tests and upload (publishing) already set up with GitHub Actions: + +- Every time you `git push` your `master` or `main` branch, the package is automatically tested against the desired Python versions with GitHub Actions. +- Every time a new release (either the initial version or an updated version) is created, the package is automatically uploaded to PyPI with GitHub Actions. + +### View it on pypi.org + +After your package is published on PyPI, go to [https://pypi.org/project/example-pypi-package/](https://pypi.org/project/example-pypi-package/) (`_` becomes `-`). Copy the command on the page, execute it to download and install your package from PyPI. (or test.pypi.org if you use that) + +
If you publish the package to PyPI manually, click to read + +### Install Twine + +Install or upgrade Twine: + +```bash +python -m pip install --user --upgrade twine +``` + +Create a **.pypirc** file in your **$HOME** (**~**) directory, its content should be: + +```ini +[pypi] +username = __token__ +password = +``` + +(Use `[testpypi]` instead of `[pypi]` if you are uploading to [TestPyPI](https://test.pypi.org/)) + +Replace `` with your real PyPI token (which starts with `pypi-`). + +(if you don't manually create **$HOME/.pypirc**, you will be prompted for a username (which should be `__token__`) and password (which should be your PyPI token) when you run Twine) + +### Upload + +Run Twine to upload all of the archives under **dist** folder: + +```bash +python -m twine upload --repository pypi dist/* +``` + +(use `testpypi` instead of `pypi` if you are uploading to [TestPyPI](https://test.pypi.org/)) + +### Update + +When you finished developing a newer version of your package, do the following things. + +Modify the version number `__version__` in **src\examplepy\_\_init\_\_.py**. + +Delete all old versions in **dist**. + +Run the following command again to regenerate **dist**: + +```bash +python setup.py sdist bdist_wheel +``` + +Run the following command again to upload **dist**: + +```bash +python -m twine upload --repository pypi dist/* +``` + +(use `testpypi` instead of `pypi` if needed) + +
+ +## References + +- [Python Packaging Authority (PyPA)'s sample project](https://github.com/pypa/sampleproject) +- [PyPA's Python Packaging User Guide](https://packaging.python.org/tutorials/packaging-projects/) +- [Stackoverflow questions and answers](https://stackoverflow.com/questions/41093648/how-to-test-that-pypi-install-will-work-before-pushing-to-pypi-python) +- [GitHub Actions Guides: Building and testing Python](https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-python) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ead8162 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=46.4.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..040b134 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +version = attr: examplepy.__version__ +license_files = LICENSE diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ebe7074 --- /dev/null +++ b/setup.py @@ -0,0 +1,53 @@ +import setuptools + +with open('README.md', 'r', encoding='utf-8') as fh: + long_description = fh.read() + +setuptools.setup( + name='example_pypi_package', + author='Tom Chen', + author_email='tomchen.org@gmail.com', + description='Example PyPI (Python Package Index) Package', + keywords='example, pypi, package', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/tomchen/example_pypi_package', + project_urls={ + 'Documentation': 'https://github.com/tomchen/example_pypi_package', + 'Bug Reports': + 'https://github.com/tomchen/example_pypi_package/issues', + 'Source Code': 'https://github.com/tomchen/example_pypi_package', + # 'Funding': '', + # 'Say Thanks!': '', + }, + package_dir={'': 'src'}, + packages=setuptools.find_packages(where='src'), + classifiers=[ + # see https://pypi.org/classifiers/ + 'Development Status :: 5 - Production/Stable', + + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Build Tools', + + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3 :: Only', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + ], + python_requires='>=3.6', + # install_requires=['Pillow'], + extras_require={ + 'dev': ['check-manifest'], + # 'test': ['coverage'], + }, + # entry_points={ + # 'console_scripts': [ # This can provide executable scripts + # 'run=examplepy:main', + # You can execute `run` in bash to run `main()` in src/examplepy/__init__.py + # ], + # }, +) diff --git a/src/examplepy/__init__.py b/src/examplepy/__init__.py new file mode 100644 index 0000000..ae6f1d7 --- /dev/null +++ b/src/examplepy/__init__.py @@ -0,0 +1,3 @@ +__version__ = "0.1.0" + +from .module1 import * diff --git a/src/examplepy/module1.py b/src/examplepy/module1.py new file mode 100644 index 0000000..f85a53b --- /dev/null +++ b/src/examplepy/module1.py @@ -0,0 +1,22 @@ +# Example PyPI (Python Package Index) Package + +class Number(object): + + def __init__(self, n): + self.value = n + + def val(self): + return self.value + + def add(self, n2): + self.value += n2.val() + + def __add__(self, n2): + return self.__class__(self.value + n2.val()) + + def __str__(self): + return str(self.val()) + + @classmethod + def addall(cls, number_obj_iter): + cls(sum(n.val() for n in number_obj_iter)) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_module1.py b/tests/test_module1.py new file mode 100644 index 0000000..d3aeede --- /dev/null +++ b/tests/test_module1.py @@ -0,0 +1,13 @@ +import unittest + +from examplepy.module1 import Number + + +class TestSimple(unittest.TestCase): + + def test_add(self): + self.assertEqual((Number(5) + Number(6)).value, 11) + + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..14d9566 --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +[tox] +envlist = py{36,37,38,39} +minversion = 3.3.0 +isolated_build = true + +[testenv] +deps = + check-manifest >= 0.42 + pytest +commands = + check-manifest --ignore 'tox.ini,tests/**,.editorconfig,vscode.env,.vscode/**' + python setup.py check -m -s + py.test tests {posargs} diff --git a/vscode.env b/vscode.env new file mode 100644 index 0000000..76297c2 --- /dev/null +++ b/vscode.env @@ -0,0 +1 @@ +PYTHONPATH=/;src/;${PYTHONPATH}