Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ on: [push]
jobs:
test:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
experimental: [false]
include:
- python-version: "3.14-dev"
experimental: true

name: Test Python ${{ matrix.python-version }}
steps:
Expand All @@ -30,10 +35,13 @@ jobs:

- name: Test with pytest
run: |
poetry run pytest --junit-xml=reports/xunit-result-${{ matrix.python-version }}.xml --cov-report=xml:reports/coverage-${{ matrix.python-version }}.xml tests
poetry run coverage run --rcfile=.coveragerc \
-m pytest --no-cov --junit-xml=reports/xunit-result-${{ matrix.python-version }}.xml tests
poetry run coverage xml --rcfile=.coveragerc \
-oreports/coverage-${{ matrix.python-version }}.xml

- name: Analyze with SonarCloud
uses: sonarsource/sonarcloud-github-action@master
uses: SonarSource/sonarqube-scan-action@v5.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
11 changes: 6 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.2.1
rev: v0.13.0
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
# Lint
- id: ruff-check
args: [ --fix ]

# Format
- id: ruff-format
23 changes: 22 additions & 1 deletion HISTORY
Original file line number Diff line number Diff line change
@@ -1,10 +1,31 @@
4.17rc2
=======

Features
--------

- Arguments no longer require Dict/Optional/Tuple etc can use dict,tuple,list
str | None


Project Changes
---------------

- Upgraded type hints to use builtin types eg `str | None` vs `Optional[str]`
- A lot of spelling and grammar fixes in doc strings
- Fixed issue causing reported test coverage to be well below expected value.
PyApp registers as a PyTest plugin to provide fixtures to simplify testing;
this causes parts of the PyApp to be imported before coverage tracking is
activated causing ~20% of SLOC not being included in the coverage report.


4.17rc1
=======

Features
--------

- Add root_execution_policy used to define behaviour of pyapp when executed as
- Add root_execution_policy used to define behaviour of pyapp when executed as
root or Administrator. The default behaviour is to deny execution. This can
be overridden by specifying an alternate policy or by using the environment
variable PYAPP_ROOT_EXECUTION_POLICY.
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes/async-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Using Async Commands
====================

As of pyApp 4.3 command handlers can be defined as Async functions and pyApp will
take care of setting up an event loop call the handler.
take care of setting up an event loop to call the handler.

.. code-block:: python

Expand Down
31 changes: 29 additions & 2 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@


@nox.session(
python=("python3.10", "python3.11", "python3.12", "python3.13", "pypy3.10"),
python=(
"python3.10",
"python3.11",
"python3.12",
"python3.13",
"pypy3.10",
"python-3.14",
),
venv_backend=None,
)
def tests(session: Session):
Expand All @@ -18,4 +25,24 @@ def tests(session: Session):
session.run("poetry", "install", "--with=dev", external=True)

print("▶️ Run tests")
session.run("poetry", "run", "pytest", "--config-file=pytest.ini", external=True)
session.run(
"poetry",
"run",
"coverage",
"run",
"--rcfile=.coveragerc",
"-m",
"pytest",
"--no-cov",
"--config-file=pytest.ini",
external=True,
)
session.run(
"poetry",
"run",
"coverage",
"xml",
"--rcfile=.coveragerc",
f"-oreports/coverage-{session.python}.xml",
external=True,
)
1,109 changes: 611 additions & 498 deletions poetry.lock

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "pyapp"
version = "4.17rc1"
version = "4.17rc2"
description = "A Python application framework - Let us handle the boring stuff!"
authors = ["Tim Savage <tim@savage.company>"]
license = "BSD-3-Clause"
Expand Down Expand Up @@ -38,16 +38,15 @@ python = "^3.10"
argcomplete = "^3.2"
colorama = "*"
yarl = "*"
importlib_metadata = {version = "*", python = "<=3.9"}
typing_extensions = "*"

pyyaml = {version = "*", optional = true }
toml = {version = "*", optional = true }

[tool.poetry.dev-dependencies]
[tool.poetry.group.dev.dependencies]
pytest = "^8.0"
pytest-cov = "^6.0"
pytest-asyncio = "^0.24"
pytest-cov = "^7.0.0"
pytest-asyncio = "^1.2"
sphinx = "^8.1"

[tool.poetry.extras]
Expand Down
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
addopts =
--cov=pyapp --cov-branch
asyncio_mode=auto
asyncio_default_fixture_loop_scope="function"
asyncio_default_fixture_loop_scope=function
48 changes: 23 additions & 25 deletions src/pyapp/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

*Application with bindings for commands*

The application object handles all the initial configuration to set up the
The application object handles all the initial configuration to set up a
run-time environment.

Quick demo::
Expand All @@ -25,12 +25,12 @@

This example provides an application with a command `hello` that takes an
optional `verbose` flag. The framework also provides help, configures and loads
settings (using :py:mod:`pyapp.conf`), an interface to the checks framework
settings (using :py:mod:`pyapp.conf`), an interface to the check framework
and configures the Python logging framework.

There are however a few more things that are required to get this going. The
There are, however, a few more things that are required to get this going. The
:py:class:`CliApplication` class expects a certain structure of your
application to allow for it's (customisable) defaults to be applied.
application to allow for its (customisable) defaults to be applied.

Your application can have one of two structures

Expand All @@ -52,10 +52,10 @@

.. versionadded:: 4.4

As of pyApp 4.4 command functions can supply all required arguments in the function
As of pyApp `4.4` command functions can supply all required arguments in the function
signature.

As an example consider the command function:
As an example, consider the command function:

.. code-block:: python

Expand All @@ -82,32 +82,32 @@ def my_command(

optional arguments:
-h, --help show this help message and exit
--arg2 Enable the argilizer
--arg2 Enable the argilizer
--arg3
--arg4 {foo,bar}


The following types are supported as arguments:

- Basic types eg int, str, float, this covers any type that can be provided
- Basic types e.g. int, str, float, this covers any type that can be provided
to argparse in the type field.

- bool, this is made into an argparse `store_true` action.

- Enum types using the pyApp EnumAction.

- Generic types
- Mapping/Dict as well as a basic dict for Key/Value pairs
- Mapping/dict as well as a basic dict for Key/Value pairs

- Sequence/List for typed sequences, ``nargs="+"`` for positional arguments
- Sequence/list for typed sequences, ``nargs="+"`` for positional arguments
of ``action="append"`` for optional.

- Tuple for typed sequences of a fixed size eg ``nargs=len(tuple)``. Only
the first type is used, the others are ignored.

- FileType from ``argparse``.

.. tip:: Too get access to the parse results from `argparse` provide a vairable
.. tip:: To get access to the parse results from `argparse` provide a variable
with the type ``pyapp.app.CommandOptions``.


Expand All @@ -123,15 +123,15 @@ def my_command(
CliApplication generates the following events, all methods are provided with the
``argparse`` namespace.

+--------------------------------------------------------------+----------------------------------------------------+
| ``pre_dispatch[[argparse.Namespace], None]`` | Generated before command dispatch is called |
+--------------------------------------------------------------+----------------------------------------------------+
| ``post_dispatch[[Optional[int], argparse.Namespace], None]`` | Generated after command dispatch returns without |
| | error includes the return code if one is provided. |
+--------------------------------------------------------------+----------------------------------------------------+
| ``dispatch_error[[Exception, argparse.Namespace], None]`` | Generated when an exception is raised in a command |
| | function before the standard exception reporting. |
+--------------------------------------------------------------+----------------------------------------------------+
+-----------------------------------------------------------+----------------------------------------------------+
| ``pre_dispatch[[argparse.Namespace], None]`` | Generated before command dispatch is called |
+-----------------------------------------------------------+----------------------------------------------------+
| ``post_dispatch[[int | None, argparse.Namespace], None]`` | Generated after command dispatch returns without |
| | error includes the return code if one is provided. |
+-----------------------------------------------------------+----------------------------------------------------+
| ``dispatch_error[[Exception, argparse.Namespace], None]`` | Generated when an exception is raised in a command |
| | function before the standard exception reporting. |
+-----------------------------------------------------------+----------------------------------------------------+


Arguments
Expand Down Expand Up @@ -472,8 +472,7 @@ def pre_configure_logging(self):
@staticmethod
def register_factories():
"""Register any abstract interface factories."""
# pylint: disable=import-outside-toplevel
from asyncio import AbstractEventLoop, get_event_loop
from asyncio import AbstractEventLoop, get_event_loop # noqa: PLC0415

register_factory(AbstractEventLoop, get_event_loop)

Expand Down Expand Up @@ -565,8 +564,7 @@ def configure_logging(self, opts: CommandOptions):

def checks_on_startup(self, opts: CommandOptions):
"""Run checks on startup."""
# pylint: disable=import-outside-toplevel
from pyapp.checks.report import execute_report
from pyapp.checks.report import execute_report # noqa: PLC0415

if opts.checks_on_startup:
out = io.StringIO()
Expand Down Expand Up @@ -629,7 +627,7 @@ def dispatch(self, args: Sequence[str] = None) -> None:
try:
exit_code = self.dispatch_handler(opts)

except Exception as ex: # pylint: disable=broad-except
except Exception as ex:
self.dispatch_error(ex, opts)
if not self.exception_report(ex, opts):
raise
Expand Down
4 changes: 2 additions & 2 deletions src/pyapp/app/argument_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def __call__(

class _EnumAction(Action):
def __init__(self, **kwargs):
enum = kwargs.pop("type", None)
enum: type | None = kwargs.pop("type", None)
if enum is None:
raise ValueError("type must be assigned an Enum when using EnumAction")
if not issubclass(enum, Enum):
Expand Down Expand Up @@ -242,7 +242,7 @@ def _copy_items(items):
if isinstance(items, list):
return items[:]

import copy # pylint: disable=import-outside-toplevel
import copy # noqa: PLC0415

return copy.copy(items)

Expand Down
Loading
Loading