Skip to content

Commit 9caaf9d

Browse files
authored
Add type annotations and mypy CI to flatdata-py and flatdata-generator (#262)
Adds type annotations to all source functions/methods in both Python projects and enforces them via mypy in CI. ## What changed **Type annotations** added. **mypy configuration** (both `pyproject.toml`): 9 strictness flags enabled — `disallow_untyped_defs`, `check_untyped_defs`, `disallow_any_generics`, `warn_return_any`, `warn_unused_configs`, `warn_redundant_casts`, `warn_unused_ignores`, `no_implicit_optional`, `strict_equality`. Namespace package settings for the shared `flatdata/` namespace. **CI**: mypy steps added to `generator.yml` and `py.yml`, running after tests. **PEP 561**: `py.typed` markers in `flatdata/lib/` and `flatdata/generator/`.
1 parent d0d05b2 commit 9caaf9d

54 files changed

Lines changed: 895 additions & 562 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/generator.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ jobs:
2121
cd flatdata-generator
2222
uv run --with pytest pytest -v
2323
pip install .
24-
flatdata-generator --help
24+
flatdata-generator --help
25+
- name: Type check
26+
run: |
27+
cd flatdata-generator
28+
uv run --with mypy mypy flatdata/

.github/workflows/py.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ jobs:
2121
cd flatdata-py
2222
uv venv
2323
uv pip install ../flatdata-generator
24-
uv pip install ".[inspector]" pytest
24+
uv pip install ".[inspector]" pytest mypy
2525
.venv/bin/pytest -v
2626
.venv/bin/flatdata-inspector --help
27+
- name: Type check
28+
run: |
29+
cd flatdata-py
30+
.venv/bin/mypy flatdata/
2731

flatdata-generator/flatdata/generator/app.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from flatdata.generator.tree.errors import FlatdataSyntaxError
2222

2323

24-
def _parse_command_line():
24+
def _parse_command_line() -> argparse.Namespace:
2525
parser = argparse.ArgumentParser(
2626
description="Generates code for a given flatdata schema file.")
2727
parser.add_argument("-s", "--schema", type=str, required=True,
@@ -39,7 +39,7 @@ def _parse_command_line():
3939
return parser.parse_args()
4040

4141

42-
def _setup_logging(args):
42+
def _setup_logging(args: argparse.Namespace) -> None:
4343
level = logging.WARNING
4444
if args.debug:
4545
level = logging.DEBUG
@@ -52,13 +52,13 @@ def _setup_logging(args):
5252
level=level)
5353

5454

55-
def _check_args(args):
55+
def _check_args(args: argparse.Namespace) -> None:
5656
if not os.path.isfile(args.schema):
5757
logging.fatal("Cannot find schema file at %s", args.schema)
5858
sys.exit(1)
5959

6060

61-
def _run(args):
61+
def _run(args: argparse.Namespace) -> None:
6262
_setup_logging(args)
6363
_check_args(args)
6464

@@ -86,6 +86,6 @@ def _run(args):
8686
logging.info("Code for %s is written to %s", args.gen, args.output_file)
8787

8888

89-
def main():
89+
def main() -> None:
9090
"""Entrypoint"""
9191
_run(_parse_command_line())

flatdata-generator/flatdata/generator/engine.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
'''
55

66
import types
7+
from typing import overload
78

89
from flatdata.generator.tree.builder import build_ast
910
from flatdata.generator.tree.nodes.trivial.namespace import Namespace
1011
from flatdata.generator.tree.nodes.node import Node
12+
from flatdata.generator.tree.syntax_tree import SyntaxTree
1113

1214
from .generators.cpp import CppGenerator
1315
from .generators.dot import DotGenerator
1416
from .generators.go import GoGenerator
1517
from .generators.python import PythonGenerator
1618
from .generators.rust import RustGenerator
1719
from .generators.flatdata import FlatdataGenerator
20+
from .generators import BaseGenerator
1821

1922

2023
class Engine:
@@ -23,7 +26,7 @@ class Engine:
2326
Implements code generation from the given flatdata schema.
2427
"""
2528

26-
_GENERATORS = {
29+
_GENERATORS: dict[str, type[BaseGenerator]] = {
2730
"cpp": CppGenerator,
2831
"dot": DotGenerator,
2932
"go": GoGenerator,
@@ -33,21 +36,21 @@ class Engine:
3336
}
3437

3538
@classmethod
36-
def available_generators(cls):
39+
def available_generators(cls) -> list[str]:
3740
"""
3841
Lists names of available code generators.
3942
"""
4043
return list(cls._GENERATORS.keys())
4144

42-
def __init__(self, schema):
45+
def __init__(self, schema: str) -> None:
4346
"""
4447
Instantiates generator engine for a given schema.
4548
:raises FlatdataSyntaxError
4649
"""
4750
self.schema = schema
4851
self.tree = build_ast(schema)
4952

50-
def render(self, generator_name):
53+
def render(self, generator_name: str) -> str:
5154
"""
5255
Render schema with a given generator
5356
:param generator_name:
@@ -60,38 +63,45 @@ def render(self, generator_name):
6063
)
6164

6265
output_content = generator.render(self.tree)
63-
return output_content
66+
return str(output_content)
6467

65-
def render_python_module(self, module_name=None, archive_name=None, root_namespace=None):
68+
@overload
69+
def render_python_module(self, module_name: str | None, archive_name: str, root_namespace: str | None = None) -> tuple[types.ModuleType, type]: ...
70+
@overload
71+
def render_python_module(self, *, archive_name: str, root_namespace: str | None = None) -> tuple[types.ModuleType, type]: ...
72+
@overload
73+
def render_python_module(self, module_name: str | None = None, archive_name: None = None, root_namespace: str | None = None) -> types.ModuleType: ...
74+
75+
def render_python_module(self, module_name: str | None = None, archive_name: str | None = None, root_namespace: str | None = None) -> types.ModuleType | tuple[types.ModuleType, type]:
6676
"""
6777
Render python module.
6878
:param module_name: Module name to use. If none, root namespace name is used.
6979
:param archive_name: Archive name to lookup,
7080
if specified, archive type is returned along with the model
7181
:param root_namespace: Root namespace to pick in case of multiple top level namespaces.
7282
"""
73-
root_namespace = self._find_root_namespace(self.tree, archive_name, root_namespace)
83+
ns = self._find_root_namespace(self.tree, archive_name, root_namespace)
7484
module_code = self.render("py")
75-
module = types.ModuleType(module_name if module_name is not None else root_namespace.name)
85+
module = types.ModuleType(module_name if module_name is not None else ns.name)
7686
#pylint: disable=exec-used
7787
exec(module_code, module.__dict__)
7888
if archive_name is None:
7989
return module
8090

81-
name = root_namespace.name + "_" + archive_name
82-
archive_type = getattr(module, name) if archive_name else None
91+
name = ns.name + "_" + archive_name
92+
archive_type = getattr(module, name)
8393
return module, archive_type
8494

8595
@classmethod
86-
def _create_generator(cls, name):
96+
def _create_generator(cls, name: str) -> BaseGenerator | None:
8797
generator_type = cls._GENERATORS.get(name, None)
8898
if generator_type is None:
8999
return None
90100

91-
return generator_type()
101+
return generator_type() # type: ignore[call-arg] # dict values are concrete subclasses with zero-arg __init__
92102

93103
@staticmethod
94-
def _find_root_namespace(tree, archive_name, root_namespace=None):
104+
def _find_root_namespace(tree: SyntaxTree, archive_name: str | None, root_namespace: str | None = None) -> Namespace:
95105
root_children = tree.root.children
96106
root_namespaces = [
97107
child for child in root_children

flatdata-generator/flatdata/generator/generators/__init__.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
'''
55

66
from abc import ABCMeta, abstractmethod
7+
from typing import NoReturn
8+
79
from jinja2 import Environment, PackageLoader
810
from jinja2 import nodes
911
from jinja2.ext import Extension
1012
from jinja2.exceptions import TemplateRuntimeError
13+
from jinja2.parser import Parser
1114

1215
from flatdata.generator.tree.nodes.archive import Archive
1316
from flatdata.generator.tree.nodes.trivial import Structure, Enumeration, Constant, Namespace
@@ -21,21 +24,21 @@
2124
class BaseGenerator(metaclass=ABCMeta):
2225
"""Abstract base class for Flatdata generators"""
2326

24-
def __init__(self, template):
27+
def __init__(self, template: str) -> None:
2528
self._template = template
2629

2730
@abstractmethod
28-
def supported_nodes(self):
31+
def supported_nodes(self) -> list[type]:
2932
"""List of supported nodes by this generator"""
3033
raise RuntimeError(
3134
"Derived generators must implement _supported_nodes")
3235

3336
@abstractmethod
34-
def _populate_environment(self, env):
37+
def _populate_environment(self, env: Environment) -> None:
3538
raise RuntimeError(
3639
"Derived generators must implement _populate_filters")
3740

38-
def render(self, tree):
41+
def render(self, tree: SyntaxTree) -> str:
3942
"""Generate the language implementation from the AST"""
4043
env = Environment(loader=PackageLoader('flatdata.generator', 'templates'), lstrip_blocks=True,
4144
trim_blocks=True, autoescape=False, extensions=[RaiseExtension])
@@ -71,7 +74,7 @@ class RaiseExtension(Extension):
7174

7275
tags = set(['raise'])
7376

74-
def parse(self, parser):
77+
def parse(self, parser: Parser) -> nodes.CallBlock:
7578
"""The first token is the line number, followed by the expression"""
7679
lineno = next(parser.stream).lineno
7780
message_node = parser.parse_expression()
@@ -81,6 +84,6 @@ def parse(self, parser):
8184
)
8285

8386
#pylint: disable=no-self-use
84-
def _raise(self, msg, caller):
87+
def _raise(self, msg: str, caller: object) -> NoReturn:
8588
"""Helper callback."""
8689
raise TemplateRuntimeError(msg)

flatdata-generator/flatdata/generator/generators/cpp.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
See the LICENSE file in the root of this project for license details.
44
'''
55

6+
from jinja2 import Environment
7+
8+
from flatdata.generator.tree.helpers.basictype import BasicType
9+
from flatdata.generator.tree.helpers.enumtype import EnumType
10+
from flatdata.generator.tree.nodes.node import Node
11+
from flatdata.generator.tree.nodes.references import BuiltinStructureReference, StructureReference
612
from flatdata.generator.tree.nodes.resources import Vector, Multivector, Instance, RawData, BoundResource, \
713
ResourceBase, Archive as ArchiveResource
814
from flatdata.generator.tree.nodes.trivial import Structure, Enumeration, Constant, Field
@@ -13,21 +19,21 @@
1319
class CppGenerator(BaseGenerator):
1420
"""Flatdata to C++ header file generator"""
1521

16-
def __init__(self):
22+
def __init__(self) -> None:
1723
BaseGenerator.__init__(self, "cpp/cpp.jinja2")
1824

19-
def supported_nodes(self):
25+
def supported_nodes(self) -> list[type]:
2026
return [Structure, Archive, Constant, Enumeration]
2127

22-
def _populate_environment(self, env):
28+
def _populate_environment(self, env: Environment) -> None:
2329
env.filters["cpp_doc"] = lambda value: value
2430

25-
def _safe_cpp_string_line(value):
31+
def _safe_cpp_string_line(value: str) -> str:
2632
return value.replace('\\', '\\\\').replace('"', r'\"')
2733

2834
env.filters["safe_cpp_string_line"] = _safe_cpp_string_line
2935

30-
def _cpp_base_type(flatdata_type):
36+
def _cpp_base_type(flatdata_type: BasicType | EnumType | Node) -> str:
3137
type_map = {
3238
"bool": "bool",
3339
"i8": "int8_t",
@@ -41,28 +47,28 @@ def _cpp_base_type(flatdata_type):
4147
}
4248
if flatdata_type.name in type_map:
4349
return type_map[flatdata_type.name]
44-
return flatdata_type.name.replace("@@", "::").replace("@", "::")
50+
return str(flatdata_type.name.replace("@@", "::").replace("@", "::"))
4551

4652
env.filters["cpp_base_type"] = _cpp_base_type
4753

48-
def _to_type_params(refs):
54+
def _to_type_params(refs: list[BuiltinStructureReference | StructureReference]) -> str:
4955
return ', '.join([ref.node.path_with("::") for ref in refs])
5056

5157
env.filters["to_type_params"] = _to_type_params
5258

53-
def _snake_to_upper_camel_case(expr):
59+
def _snake_to_upper_camel_case(expr: str) -> str:
5460
return ''.join(p.title() for p in expr.split('_'))
5561

5662
env.filters["snake_to_upper_camel_case"] = _snake_to_upper_camel_case
5763

58-
def _typedef_name(entity, extra_suffix=""):
64+
def _typedef_name(entity: Field | ResourceBase, extra_suffix: str = "") -> str:
5965
assert isinstance(entity, (Field, ResourceBase)), "Got: %s" % entity.__class__
6066
return _snake_to_upper_camel_case(entity.name) + extra_suffix + "Type"
6167

6268
env.filters["typedef_name"] = _typedef_name
6369

64-
def _optional_typedef_usage(resource, extra_suffix=""):
65-
def _wrap_in_optional(declaration):
70+
def _optional_typedef_usage(resource: ResourceBase, extra_suffix: str = "") -> str:
71+
def _wrap_in_optional(declaration: str) -> str:
6672
if resource.optional:
6773
return "boost::optional< %s >" % declaration
6874
return declaration
@@ -71,7 +77,7 @@ def _wrap_in_optional(declaration):
7177

7278
env.filters["archive_typedef_usage"] = _optional_typedef_usage
7379

74-
def _resource_provides_incremental_builder(resource):
80+
def _resource_provides_incremental_builder(resource: ResourceBase) -> bool:
7581
assert isinstance(resource, ResourceBase)
7682
if isinstance(resource, Instance):
7783
return False
@@ -86,7 +92,7 @@ def _resource_provides_incremental_builder(resource):
8692
env.filters[
8793
"resource_provides_incremental_builder"] = _resource_provides_incremental_builder
8894

89-
def provides_setter(resource):
95+
def provides_setter(resource: ResourceBase) -> bool:
9096
assert isinstance(resource, ResourceBase)
9197
if isinstance(resource, Instance):
9298
return True

flatdata-generator/flatdata/generator/generators/dot.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,30 @@
44
'''
55

66
from flatdata.generator.tree.nodes.archive import Archive
7+
from flatdata.generator.tree.nodes.trivial import Field
78
from . import BaseGenerator
89

10+
from jinja2 import Environment
11+
912
SCOPE_SEPARATOR = "__"
1013
DECORATION_BOUND = "__bound__"
1114

1215

1316
class DotGenerator(BaseGenerator):
1417
"""Flatdata to DOT (graph description language) generator"""
1518

16-
def __init__(self):
19+
def __init__(self) -> None:
1720
BaseGenerator.__init__(self, "dot/dot.jinja2")
1821

19-
def _populate_environment(self, env):
22+
def _populate_environment(self, env: Environment) -> None:
2023
env.autoescape = True
2124

22-
def _field_value_type(field):
23-
type_name = field.type.name.replace("@@", ".").replace("@", ".")
24-
namespace_name = field.parent.parent.path
25+
def _field_value_type(field: Field) -> str:
26+
assert field.type is not None
27+
assert field.parent is not None
28+
assert field.parent.parent is not None
29+
type_name = str(field.type.name).replace("@@", ".").replace("@", ".")
30+
namespace_name = str(field.parent.parent.path)
2531
if type_name.startswith(namespace_name):
2632
type_name = type_name[len(namespace_name):]
2733
if type_name.startswith("."):
@@ -31,5 +37,5 @@ def _field_value_type(field):
3137

3238
env.filters["field_value_type"] = _field_value_type
3339

34-
def supported_nodes(self):
40+
def supported_nodes(self) -> list[type]:
3541
return [Archive]

0 commit comments

Comments
 (0)