mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-11-14 12:33:41 +00:00
Compare commits
1 Commits
203c024287
...
b36bcb194b
| Author | SHA1 | Date | |
|---|---|---|---|
| b36bcb194b |
@ -236,50 +236,16 @@ The projects also uses typing checks (provided by `mypy`) and some linter checks
|
|||||||
tox
|
tox
|
||||||
```
|
```
|
||||||
|
|
||||||
Must be usually done before any pushes.
|
|
||||||
|
|
||||||
### Generate documentation templates
|
### Generate documentation templates
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tox -e docs
|
tox -e docs
|
||||||
```
|
```
|
||||||
|
|
||||||
Must be usually done if there are changes in modules structure.
|
|
||||||
|
|
||||||
### Create release
|
### Create release
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
tox -m release -- major.minor.patch
|
tox -m release -- x.y.z
|
||||||
```
|
```
|
||||||
|
|
||||||
The command above will generate documentation, tags, etc., and will push them to GitHub. Other things will be handled by GitHub workflows automatically.
|
The command above will generate documentation, tags, etc., and will push them to GitHub. Other things will be handled by GitHub workflows automatically.
|
||||||
|
|
||||||
### Hotfixes policy
|
|
||||||
|
|
||||||
Sometimes it is required to publish hotfix with specific commits, but some features have been already committed, which should not be included to the hotfix. In this case, some manual steps are required:
|
|
||||||
|
|
||||||
1. Create new branch from the last stable release (`major.minor.patch`):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git checkout -b release/major.minor major.minor.patch
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Cherry-pick desired commit(s):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git cherry-pick <commit-sha>
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, make changes to the new branch and commit them.
|
|
||||||
|
|
||||||
3. Push newly created branch to remote:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
git push --set-upstream origin release/major.minor
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Proceed to release as usual:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
tox -m release -- major.minor.patch+1
|
|
||||||
```
|
|
||||||
|
|||||||
@ -30,14 +30,12 @@ class ImportType(StrEnum):
|
|||||||
import type enumeration
|
import type enumeration
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
Future(MethodTypeOrder): (class attribute) from __future__ import
|
|
||||||
Package(MethodTypeOrder): (class attribute) package import
|
Package(MethodTypeOrder): (class attribute) package import
|
||||||
PackageFrom(MethodTypeOrder): (class attribute) package import, from clause
|
PackageFrom(MethodTypeOrder): (class attribute) package import, from clause
|
||||||
System(ImportType): (class attribute) system installed packages
|
System(ImportType): (class attribute) system installed packages
|
||||||
SystemFrom(MethodTypeOrder): (class attribute) system installed packages, from clause
|
SystemFrom(MethodTypeOrder): (class attribute) system installed packages, from clause
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Future = "future"
|
|
||||||
Package = "package"
|
Package = "package"
|
||||||
PackageFrom = "package-from"
|
PackageFrom = "package-from"
|
||||||
System = "system"
|
System = "system"
|
||||||
@ -72,7 +70,6 @@ class ImportOrder(BaseRawFileChecker):
|
|||||||
"import-type-order",
|
"import-type-order",
|
||||||
{
|
{
|
||||||
"default": [
|
"default": [
|
||||||
"future",
|
|
||||||
"system",
|
"system",
|
||||||
"system-from",
|
"system-from",
|
||||||
"package",
|
"package",
|
||||||
@ -94,7 +91,7 @@ class ImportOrder(BaseRawFileChecker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def imports(source: Iterable[Any], start_lineno: int = 0) -> Iterable[nodes.Import | nodes.ImportFrom]:
|
def imports(source: Iterable[Any], start_lineno: int = 0) -> list[nodes.Import | nodes.ImportFrom]:
|
||||||
"""
|
"""
|
||||||
extract import nodes from list of raw nodes
|
extract import nodes from list of raw nodes
|
||||||
|
|
||||||
@ -103,7 +100,7 @@ class ImportOrder(BaseRawFileChecker):
|
|||||||
start_lineno(int, optional): minimal allowed line number (Default value = 0)
|
start_lineno(int, optional): minimal allowed line number (Default value = 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Iterable[nodes.Import | nodes.ImportFrom]: list of import nodes
|
list[nodes.Import | nodes.ImportFrom]: list of import nodes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def is_defined_import(imports: Any) -> bool:
|
def is_defined_import(imports: Any) -> bool:
|
||||||
@ -111,7 +108,7 @@ class ImportOrder(BaseRawFileChecker):
|
|||||||
and imports.lineno is not None \
|
and imports.lineno is not None \
|
||||||
and imports.lineno >= start_lineno
|
and imports.lineno >= start_lineno
|
||||||
|
|
||||||
return sorted(filter(is_defined_import, source), key=lambda imports: imports.lineno)
|
return list(filter(is_defined_import, source))
|
||||||
|
|
||||||
def check_from_imports(self, imports: nodes.ImportFrom) -> None:
|
def check_from_imports(self, imports: nodes.ImportFrom) -> None:
|
||||||
"""
|
"""
|
||||||
@ -127,36 +124,30 @@ class ImportOrder(BaseRawFileChecker):
|
|||||||
self.add_message("from-imports-out-of-order", line=imports.lineno, args=(real, expected))
|
self.add_message("from-imports-out-of-order", line=imports.lineno, args=(real, expected))
|
||||||
break
|
break
|
||||||
|
|
||||||
def check_imports(self, imports: Iterable[nodes.Import | nodes.ImportFrom], root_package: str) -> None:
|
def check_imports(self, imports: list[nodes.Import | nodes.ImportFrom], root_package: str) -> None:
|
||||||
"""
|
"""
|
||||||
check imports
|
check imports
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
imports(Iterable[nodes.Import | nodes.ImportFrom]): list of imports in their defined order
|
imports(list[nodes.Import | nodes.ImportFrom]): list of imports in their defined order
|
||||||
root_package(str): root package name
|
root_package(str): root package name
|
||||||
"""
|
"""
|
||||||
last_statement: tuple[int, str] | None = None
|
last_statement: tuple[int, str] | None = None
|
||||||
|
|
||||||
for statement in imports:
|
for statement in imports:
|
||||||
# define types and perform specific checks
|
# define types and perform specific checks
|
||||||
match statement:
|
if isinstance(statement, nodes.ImportFrom):
|
||||||
case nodes.ImportFrom() if statement.modname == "__future__":
|
import_name = statement.modname
|
||||||
import_name = statement.modname
|
root, *_ = import_name.split(".", maxsplit=1)
|
||||||
import_type = ImportType.Future
|
import_type = ImportType.PackageFrom if root_package == root else ImportType.SystemFrom
|
||||||
case nodes.ImportFrom():
|
# check from import itself
|
||||||
import_name = statement.modname
|
self.check_from_imports(statement)
|
||||||
root, *_ = import_name.split(".", maxsplit=1)
|
else:
|
||||||
import_type = ImportType.PackageFrom if root_package == root else ImportType.SystemFrom
|
import_name = next(name for name, _ in statement.names)
|
||||||
# check from import itself
|
root, *_ = import_name.split(".", maxsplit=1)[0]
|
||||||
self.check_from_imports(statement)
|
import_type = ImportType.Package if root_package == root else ImportType.System
|
||||||
case nodes.Import():
|
# check import itself
|
||||||
import_name = next(name for name, _ in statement.names)
|
self.check_package_imports(statement)
|
||||||
root, *_ = import_name.split(".", maxsplit=1)
|
|
||||||
import_type = ImportType.Package if root_package == root else ImportType.System
|
|
||||||
# check import itself
|
|
||||||
self.check_package_imports(statement)
|
|
||||||
case _:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# extract index
|
# extract index
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -21,8 +21,6 @@ import argparse
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import ahriman.application.handlers
|
|
||||||
|
|
||||||
from ahriman import __version__
|
from ahriman import __version__
|
||||||
from ahriman.application.handlers.handler import Handler
|
from ahriman.application.handlers.handler import Handler
|
||||||
from ahriman.application.help_formatter import _HelpFormatter
|
from ahriman.application.help_formatter import _HelpFormatter
|
||||||
@ -89,7 +87,8 @@ Start web service (requires additional configuration):
|
|||||||
|
|
||||||
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command")
|
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command")
|
||||||
|
|
||||||
for handler in implementations(ahriman.application.handlers, Handler):
|
handlers_root = Path(__file__).parent / "handlers"
|
||||||
|
for handler in implementations(handlers_root, "ahriman.application.handlers", Handler):
|
||||||
for subparser_parser in handler.arguments:
|
for subparser_parser in handler.arguments:
|
||||||
subparser = subparser_parser(subparsers)
|
subparser = subparser_parser(subparsers)
|
||||||
subparser.formatter_class = _HelpFormatter
|
subparser.formatter_class = _HelpFormatter
|
||||||
|
|||||||
@ -17,14 +17,14 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import aiohttp_security
|
import aiohttp_security
|
||||||
_has_aiohttp_security = True
|
_has_aiohttp_security = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_has_aiohttp_security = False
|
_has_aiohttp_security = False
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["authorized_userid", "check_authorized", "forget", "remember"]
|
__all__ = ["authorized_userid", "check_authorized", "forget", "remember"]
|
||||||
|
|
||||||
|
|||||||
@ -17,8 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
# pylint: disable=imports-out-of-order
|
from logging import NullHandler # pylint: disable=imports-out-of-order
|
||||||
from logging import NullHandler
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ from collections.abc import Generator
|
|||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from pkgutil import ModuleInfo, walk_packages
|
from pkgutil import ModuleInfo, walk_packages
|
||||||
from types import ModuleType
|
|
||||||
from typing import Any, TypeGuard, TypeVar
|
from typing import Any, TypeGuard, TypeVar
|
||||||
|
|
||||||
|
|
||||||
@ -52,12 +51,13 @@ def _modules(module_root: Path, prefix: str) -> Generator[ModuleInfo, None, None
|
|||||||
yield module_info
|
yield module_info
|
||||||
|
|
||||||
|
|
||||||
def implementations(root_module: ModuleType, base_class: type[T]) -> Generator[type[T], None, None]:
|
def implementations(module_root: Path, prefix: str, base_class: type[T]) -> Generator[type[T], None, None]:
|
||||||
"""
|
"""
|
||||||
extract implementations of the ``base_class`` from the module
|
extract implementations of the ``base_class`` from the module
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
root_module(ModuleType): root module
|
module_root(Path): root module path with implementations
|
||||||
|
prefix(str): modules package prefix
|
||||||
base_class(type[T]): base class type
|
base_class(type[T]): base class type
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
@ -68,11 +68,7 @@ def implementations(root_module: ModuleType, base_class: type[T]) -> Generator[t
|
|||||||
and issubclass(clazz, base_class) and clazz != base_class \
|
and issubclass(clazz, base_class) and clazz != base_class \
|
||||||
and clazz.__module__ == module.__name__
|
and clazz.__module__ == module.__name__
|
||||||
|
|
||||||
prefix = root_module.__name__
|
for module_info in _modules(module_root, prefix):
|
||||||
|
module = import_module(module_info.name)
|
||||||
for module_root in root_module.__path__:
|
for _, attribute in inspect.getmembers(module, is_base_class):
|
||||||
for module_info in _modules(Path(module_root), prefix):
|
yield attribute
|
||||||
module = import_module(module_info.name)
|
|
||||||
|
|
||||||
for _, attribute in inspect.getmembers(module, is_base_class):
|
|
||||||
yield attribute
|
|
||||||
|
|||||||
@ -18,8 +18,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from aiohttp.web import Application, View
|
from aiohttp.web import Application, View
|
||||||
|
from pathlib import Path
|
||||||
import ahriman.web.views
|
|
||||||
|
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.module_loader import implementations
|
from ahriman.core.module_loader import implementations
|
||||||
@ -29,18 +28,19 @@ from ahriman.web.views.base import BaseView
|
|||||||
__all__ = ["setup_routes"]
|
__all__ = ["setup_routes"]
|
||||||
|
|
||||||
|
|
||||||
def _dynamic_routes(configuration: Configuration) -> dict[str, type[View]]:
|
def _dynamic_routes(module_root: Path, configuration: Configuration) -> dict[str, type[View]]:
|
||||||
"""
|
"""
|
||||||
extract dynamic routes based on views
|
extract dynamic routes based on views
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
module_root(Path): root module path with views
|
||||||
configuration(Configuration): configuration instance
|
configuration(Configuration): configuration instance
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict[str, type[View]]: map of the route to its view
|
dict[str, type[View]]: map of the route to its view
|
||||||
"""
|
"""
|
||||||
routes: dict[str, type[View]] = {}
|
routes: dict[str, type[View]] = {}
|
||||||
for view in implementations(ahriman.web.views, BaseView):
|
for view in implementations(module_root, "ahriman.web.views", BaseView):
|
||||||
view_routes = view.routes(configuration)
|
view_routes = view.routes(configuration)
|
||||||
routes.update([(route, view) for route in view_routes])
|
routes.update([(route, view) for route in view_routes])
|
||||||
|
|
||||||
@ -57,5 +57,6 @@ def setup_routes(application: Application, configuration: Configuration) -> None
|
|||||||
"""
|
"""
|
||||||
application.router.add_static("/static", configuration.getpath("web", "static_path"), follow_symlinks=True)
|
application.router.add_static("/static", configuration.getpath("web", "static_path"), follow_symlinks=True)
|
||||||
|
|
||||||
for route, view in _dynamic_routes(configuration).items():
|
views_root = Path(__file__).parent / "views"
|
||||||
|
for route, view in _dynamic_routes(views_root, configuration).items():
|
||||||
application.router.add_view(route, view)
|
application.router.add_view(route, view)
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
import ahriman.web.views
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from ahriman.core.module_loader import _modules, implementations
|
from ahriman.core.module_loader import _modules, implementations
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
|
|
||||||
|
|
||||||
def test_implementations() -> None:
|
def test_implementations(resource_path_root: Path) -> None:
|
||||||
"""
|
"""
|
||||||
must load implementations from the package
|
must load implementations from the package
|
||||||
"""
|
"""
|
||||||
routes = list(implementations(ahriman.web.views, BaseView))
|
views_root = resource_path_root / ".." / ".." / "src" / "ahriman" / "web" / "views"
|
||||||
|
routes = list(implementations(views_root, "ahriman.web.views", BaseView))
|
||||||
assert routes
|
assert routes
|
||||||
assert all(isinstance(view, type) for view in routes)
|
assert all(isinstance(view, type) for view in routes)
|
||||||
assert all(issubclass(view, BaseView) for view in routes)
|
assert all(issubclass(view, BaseView) for view in routes)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ def test_dynamic_routes(resource_path_root: Path, configuration: Configuration)
|
|||||||
if file.suffix == ".py" and file.name not in ("__init__.py", "base.py", "status_view_guard.py")
|
if file.suffix == ".py" and file.name not in ("__init__.py", "base.py", "status_view_guard.py")
|
||||||
]
|
]
|
||||||
|
|
||||||
routes = _dynamic_routes(configuration)
|
routes = _dynamic_routes(views_root, configuration)
|
||||||
assert all(isinstance(view, type) for view in routes.values())
|
assert all(isinstance(view, type) for view in routes.values())
|
||||||
assert len(set(routes.values())) == len(expected_views)
|
assert len(set(routes.values())) == len(expected_views)
|
||||||
|
|
||||||
|
|||||||
4
tox.ini
4
tox.ini
@ -27,6 +27,8 @@ description = Run common checks like linter, mypy, etc
|
|||||||
deps =
|
deps =
|
||||||
{[tox]dependencies}
|
{[tox]dependencies}
|
||||||
-e .[check]
|
-e .[check]
|
||||||
|
allowlist_externals =
|
||||||
|
bash
|
||||||
setenv =
|
setenv =
|
||||||
MYPYPATH=src
|
MYPYPATH=src
|
||||||
commands =
|
commands =
|
||||||
@ -34,7 +36,7 @@ commands =
|
|||||||
pylint --rcfile=.pylintrc "src/{[tox]project_name}"
|
pylint --rcfile=.pylintrc "src/{[tox]project_name}"
|
||||||
bandit -c .bandit.yml -r "src/{[tox]project_name}"
|
bandit -c .bandit.yml -r "src/{[tox]project_name}"
|
||||||
bandit -c .bandit-test.yml -r "tests/{[tox]project_name}"
|
bandit -c .bandit-test.yml -r "tests/{[tox]project_name}"
|
||||||
mypy {[mypy]flags} -p "{[tox]project_name}" --install-types --non-interactive
|
bash -c 'mypy {[mypy]flags} -p "{[tox]project_name}" --install-types --non-interactive || mypy {[mypy]flags} -p "{[tox]project_name}"'
|
||||||
|
|
||||||
[testenv:docs]
|
[testenv:docs]
|
||||||
description = Generate source files for documentation
|
description = Generate source files for documentation
|
||||||
|
|||||||
Reference in New Issue
Block a user