diff --git a/.drone.jsonnet b/.drone.jsonnet index 732497b..3cef70b 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -17,16 +17,9 @@ local BuildAndTestPipeline(name, image) = { name: "Install package and test", image: image, commands: [ - "echo Install package", - "pip install -U setuptools wheel pip; pip install .", - "echo Test to import module of package", - "python -c \"import importlib, setuptools; [print(importlib.import_module(package).__name__, '[OK]') for package in setuptools.find_packages() if package.startswith('aiohttp_pydantic.') or package == 'aiohttp_pydantic']\"", - "echo Install CI dependencies", - "pip install -r requirements/ci.txt", - "echo Launch unittest", - "pytest --cov-report=xml --cov=aiohttp_pydantic tests/", - "echo Check the README.rst render", - "python -m readme_renderer -o /dev/null README.rst" + "test \"$(md5sum tasks.py)\" = \"18f864b3ac76119938e3317e49b4ffa1 tasks.py\"", + "pip install -U setuptools wheel pip; pip install invoke", + "invoke prepare-upload" ] }, { @@ -56,7 +49,7 @@ local BuildAndTestPipeline(name, image) = { steps: [ { name: "Install twine and deploy", - image: "python3.8", + image: "python:3.8", environment: { pypi_username: { from_secret: 'pypi_username' @@ -66,10 +59,9 @@ local BuildAndTestPipeline(name, image) = { } }, commands: [ - "pip install --force-reinstall twine wheel", - "python setup.py build bdist_wheel", - "set +x", - "twine upload --non-interactive -u \"$pypi_username\" -p \"$pypi_password\" dist/*" + "test \"$(md5sum tasks.py)\" = \"18f864b3ac76119938e3317e49b4ffa1 tasks.py\"", + "pip install -U setuptools wheel pip; pip install invoke", + "invoke upload --pypi-user \"$pypi_username\" --pypi-password \"$pypi_password\"" ] }, ], diff --git a/.drone.yml b/.drone.yml index cc2ef11..c623f97 100644 --- a/.drone.yml +++ b/.drone.yml @@ -11,16 +11,9 @@ steps: - name: Install package and test image: python:3.8 commands: - - echo Install package - - pip install -U setuptools wheel pip; pip install . - - echo Test to import module of package - - python -c "import importlib, setuptools; [print(importlib.import_module(package).__name__, '[OK]') for package in setuptools.find_packages() if package.startswith('aiohttp_pydantic.') or package == 'aiohttp_pydantic']" - - echo Install CI dependencies - - pip install -r requirements/ci.txt - - echo Launch unittest - - pytest --cov-report=xml --cov=aiohttp_pydantic tests/ - - echo Check the README.rst render - - python -m readme_renderer -o /dev/null README.rst + - test "$(md5sum tasks.py)" = "18f864b3ac76119938e3317e49b4ffa1 tasks.py" + - pip install -U setuptools wheel pip; pip install invoke + - invoke prepare-upload - name: coverage image: plugins/codecov @@ -48,16 +41,9 @@ steps: - name: Install package and test image: python:3.9 commands: - - echo Install package - - pip install -U setuptools wheel pip; pip install . - - echo Test to import module of package - - python -c "import importlib, setuptools; [print(importlib.import_module(package).__name__, '[OK]') for package in setuptools.find_packages() if package.startswith('aiohttp_pydantic.') or package == 'aiohttp_pydantic']" - - echo Install CI dependencies - - pip install -r requirements/ci.txt - - echo Launch unittest - - pytest --cov-report=xml --cov=aiohttp_pydantic tests/ - - echo Check the README.rst render - - python -m readme_renderer -o /dev/null README.rst + - test "$(md5sum tasks.py)" = "18f864b3ac76119938e3317e49b4ffa1 tasks.py" + - pip install -U setuptools wheel pip; pip install invoke + - invoke prepare-upload - name: coverage image: plugins/codecov @@ -83,12 +69,11 @@ platform: steps: - name: Install twine and deploy - image: python3.8 + image: python:3.8 commands: - - pip install --force-reinstall twine wheel - - python setup.py build bdist_wheel - - set +x - - twine upload --non-interactive -u "$pypi_username" -p "$pypi_password" dist/* + - test "$(md5sum tasks.py)" = "18f864b3ac76119938e3317e49b4ffa1 tasks.py" + - pip install -U setuptools wheel pip; pip install invoke + - invoke upload --pypi-user "$pypi_username" --pypi-password "$pypi_password" environment: pypi_password: from_secret: pypi_password @@ -105,6 +90,6 @@ depends_on: --- kind: signature -hmac: dfd0429e3b9f364147c56a400cf37466d0cbf0966e613f11b726777553fd9931 +hmac: 9a24ccae6182723af71257495d7843fd40874006c5e867cdebf363f497ddb839 ... diff --git a/.gitignore b/.gitignore index f6640f4..f3f07b7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ aiohttp_pydantic.egg-info/ build/ coverage.xml dist/ +dist_venv/ +venv/ \ No newline at end of file diff --git a/requirements/ci.txt b/requirements/ci.txt index 30b16b6..1bb68d6 100644 --- a/requirements/ci.txt +++ b/requirements/ci.txt @@ -1,28 +1,42 @@ -aiohttp==3.7.3 async-timeout==3.0.1 attrs==21.2.0 -bleach==3.3.0 +bleach==4.0.0 certifi==2021.5.30 -chardet==3.0.4 +cffi==1.14.6 +chardet==4.0.0 +charset-normalizer==2.0.4 codecov==2.1.11 +colorama==0.4.4 coverage==5.5 +cryptography==3.4.7 docutils==0.17.1 -idna==2.10 +idna==3.2 +importlib-metadata==4.6.3 iniconfig==1.1.1 +jeepney==0.7.1 +keyring==23.0.1 multidict==5.1.0 packaging==21.0 +pkginfo==1.7.1 pluggy==0.13.1 py==1.10.0 +pycparser==2.20 Pygments==2.9.0 pyparsing==2.4.7 pytest==6.1.2 pytest-aiohttp==0.3.0 pytest-cov==2.10.1 readme-renderer==29.0 -requests==2.25.1 +requests==2.26.0 +requests-toolbelt==0.9.1 +rfc3986==1.5.0 +SecretStorage==3.3.1 six==1.16.0 toml==0.10.2 +tqdm==4.62.0 +twine==3.4.2 typing-extensions==3.10.0.0 urllib3==1.26.6 webencodings==0.5.1 -yarl==1.6.3 \ No newline at end of file +yarl==1.6.3 +zipp==3.5.0 diff --git a/requirements/test.txt b/requirements/test.txt index cd9319c..5228fdc 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,14 +1,23 @@ -attrs-21.2.0 -coverage-5.5 +async-timeout==3.0.1 +attrs==21.2.0 +bleach==4.0.0 +chardet==4.0.0 +coverage==5.5 +docutils==0.17.1 +idna==3.2 iniconfig==1.1.1 +multidict==5.1.0 packaging==21.0 pluggy==0.13.1 py==1.10.0 +Pygments==2.9.0 pyparsing==2.4.7 -pytest==6.2.4 +pytest==6.1.2 pytest-aiohttp==0.3.0 -pytest-cov-2.12.1 -pyyaml==5.3.1 -six==1.15.0 +pytest-cov==2.10.1 +readme-renderer==29.0 +six==1.16.0 toml==0.10.2 -typing-extensions==3.7.4.3 +typing-extensions==3.10.0.0 +webencodings==0.5.1 +yarl==1.6.3 diff --git a/setup.cfg b/setup.cfg index f6377a4..39a51f7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,13 +35,20 @@ install_requires = swagger-ui-bundle [options.extras_require] -test = pytest==6.1.2; pytest-aiohttp==0.3.0; pytest-cov==2.10.1 -ci = pytest==6.1.2; pytest-aiohttp==0.3.0; pytest-cov==2.10.1; codecov==2.1.11; readme-renderer==29.0 +test = + pytest==6.1.2 + pytest-aiohttp==0.3.0 + pytest-cov==2.10.1 + readme-renderer==29.0 +ci = + %(test)s + codecov==2.1.11 + twine==3.4.2 [options.packages.find] exclude = - tests - demo + tests* + demo* [options.package_data] aiohttp_pydantic.oas = index.j2 diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..96bf572 --- /dev/null +++ b/tasks.py @@ -0,0 +1,172 @@ +""" +To use this module, install invoke and type invoke -l +""" + +from functools import partial +import os +from pathlib import Path +from setuptools.config import read_configuration + +from invoke import task, Exit, Task as Task_, call + + +def activate_venv(c, venv: str): + """ + Activate a virtualenv + """ + virtual_env = Path().absolute() / venv + if original_path := os.environ.get("PATH"): + path = f'{virtual_env / "bin"}:{original_path}' + else: + path = str(virtual_env / "bin") + c.config.run.env["PATH"] = path + c.config.run.env["VIRTUAL_ENV"] = str(virtual_env) + os.environ.pop("PYTHONHOME", "") + + +def title(text, underline_char="#"): + """ + Display text as a title. + """ + template = f"{{:{underline_char}^80}}" + text = template.format(f" {text.strip()} ") + print(f"\033[1m{text}\033[0m") + + +class Task(Task_): + """ + This task add 'skip_if_recent' feature. + + >>> @task(skip_if_recent=['./target', './dependency']) + >>> def my_tash(c): + >>> ... + + target is file created by the task + dependency is file used by the task + + The task is ran only if the dependency is more recent than the target file. + The target or the dependency can be a tuple of files. + """ + + def __init__(self, *args, **kwargs): + self.skip_if_recent = kwargs.pop("skip_if_recent", None) + super().__init__(*args, **kwargs) + + def __call__(self, *args, **kwargs): + title(self.__doc__ or self.name) + + if self.skip_if_recent: + targets, dependencies = self.skip_if_recent + if isinstance(targets, str): + targets = (targets,) + if isinstance(dependencies, str): + dependencies = (dependencies,) + + target_mtime = min( + ((Path(file).exists() and Path(file).lstat().st_mtime) or 0) + for file in targets + ) + dependency_mtime = max(Path(file).lstat().st_mtime for file in dependencies) + + if dependency_mtime < target_mtime: + print(f"{self.name}, nothing to do") + return None + + return super().__call__(*args, **kwargs) + + +task = partial(task, klass=Task) + + +@task() +def venv(c): + """ + Create a virtual environment for dev + """ + c.run("python -m venv --clear venv") + c.run("venv/bin/pip install -U setuptools wheel pip") + c.run("venv/bin/pip install -e .") + c.run("venv/bin/pip install -r requirements/test.txt") + + +@task() +def check_readme(c): + """ + Check the README.rst render + """ + c.run("python -m readme_renderer -o /dev/null README.rst") + + +@task() +def test(c, isolate=False): + """ + Launch tests + """ + opt = "I" if isolate else "" + c.run(f"python -{opt}m pytest --cov-report=xml --cov=aiohttp_pydantic tests/") + + +@task() +def tag_eq_version(c): + """ + Ensure that the last git tag matches the package version + """ + git_tag = c.run("git describe --tags HEAD", hide=True).stdout.strip() + package_version = read_configuration("./setup.cfg")["metadata"]["version"] + if git_tag != f"v{package_version}": + raise Exit( + f"ERROR: The git tag {git_tag!r} does not matches" + f" the package version {package_version!r}" + ) + + +@task() +def prepare_ci_env(c): + """ + Prepare CI environment + """ + title("Creating virtual env", "=") + c.run("python -m venv --clear dist_venv") + activate_venv(c, "dist_venv") + + c.run("dist_venv/bin/python -m pip install -U setuptools wheel pip") + + title("Building wheel", "=") + c.run("dist_venv/bin/python setup.py build bdist_wheel") + + title("Installing wheel", "=") + package_version = read_configuration("./setup.cfg")["metadata"]["version"] + dist = next(Path("dist").glob(f"aiohttp_pydantic-{package_version}-*.whl")) + c.run(f"dist_venv/bin/python -m pip install {dist}") + + # We verify that aiohttp-pydantic module is importable before installing CI tools. + package_names = read_configuration("./setup.cfg")["options"]["packages"] + for package_name in package_names: + c.run(f"dist_venv/bin/python -I -c 'import {package_name}'") + + title("Installing CI tools", "=") + c.run("dist_venv/bin/python -m pip install -r requirements/ci.txt") + + +@task(prepare_ci_env, check_readme, call(test, isolate=True), klass=Task_) +def prepare_upload(c): + """ + Launch all tests and verifications + """ + + +@task(tag_eq_version, prepare_upload) +def upload(c, pypi_user=None, pypi_password=None): + """ + Upload on pypi + """ + package_version = read_configuration("./setup.cfg")["metadata"]["version"] + dist = next(Path("dist").glob(f"aiohttp_pydantic-{package_version}-*.whl")) + if pypi_user is not None and pypi_password is not None: + c.run( + f"dist_venv/bin/twine upload --non-interactive" + f" -u {pypi_user} -p {pypi_password} {dist}", + hide=True, + ) + else: + c.run(f"dist_venv/bin/twine upload --repository aiohttp-pydantic {dist}")