From bffe7ca5e9463b5890cae69b5206fbdd35cf3bc9 Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Mon, 6 Jul 2026 19:00:25 +0200 Subject: [PATCH 1/2] Render dataclass @property members in generated API reference The generator routed every dataclass to render_dataclass, which emitted only the field table. Computed @property members were dropped, so ActivityDetail.allocation_percent, is_allocated, inputs, outputs and the same on eight other data types were documented nowhere. Reuse _public_members to append a Properties section, matching how render_class already renders non-dataclass types. README regenerated in the same commit so the drift check stays green. --- pyvolca/README.md | 116 ++++++++++++++++++++++++++++++++++ pyvolca/scripts/gen_api_md.py | 39 +++++++----- 2 files changed, 139 insertions(+), 16 deletions(-) diff --git a/pyvolca/README.md b/pyvolca/README.md index 756c10d1..dad2a849 100644 --- a/pyvolca/README.md +++ b/pyvolca/README.md @@ -887,6 +887,47 @@ instead of walking the raw exchanges list. | `all_products` | `list[Activity]` | — | | `exchanges` | `list[Union[TechnosphereExchange, BiosphereExchange, WasteExchange]]` | — | +#### Properties + +##### `allocation_percent` + +This process's own allocation share (0..100), or ``None``. + +A multi-output process splits the parent activity's burden across its +co-products; every :attr:`all_products` entry carries its share. This +returns the share of *this* process — the entry whose ``process_id`` +matches — and ``None`` for single-output processes. + +##### `inputs` + +Every input exchange — technosphere inputs and biosphere resources. + +Equivalent to filtering :attr:`exchanges` by ``e.is_input``. Mixed +kinds: callers needing only one variant should use +:attr:`technosphere_inputs` or filter manually. + +##### `is_allocated` + +True iff the activity splits its burden across several co-products. + +Reads the structured ``allocation_percent`` the engine sets on each +:attr:`all_products` entry (authoritative), not the description text. + +##### `outputs` + +Every output exchange — products and biosphere emissions. + +Includes the reference product, coproducts (in allocated +activities), and all biosphere emissions. + +##### `technosphere_inputs` + +Only the technosphere inputs (ingredients from other activities). + +Excludes biosphere inputs (resource extractions) and waste +outputs. The common case when answering "what does this activity +consume from upstream?". + ### `ActivityDiff` Result of ``compare_activities``. @@ -910,6 +951,12 @@ One matched or unmatched flow in an activity comparison. | `right` | `float \| None` | — | | `unit` | `str \| None` | — | +#### Properties + +##### `delta` + +right - left (0 if one side is missing). + ### `AggregateGroup` One bucket inside an AggregateResult. @@ -953,6 +1000,22 @@ An exchange with the environment (resource extraction or emission). | `is_biosphere` | `bool` | True | | `is_waste` | `bool` | False | +#### Properties + +##### `is_input` + +True for resource extractions (``direction`` is ``RESOURCE``). + +Biosphere inputs are resource extractions; outputs are emissions +to the environment. + +##### `is_reference` + +Always False — biosphere exchanges cannot be reference flows. + +The reference flow defines the functional unit and is always a +technosphere product (see :class:`TechnosphereExchange.is_reference`). + ### `CharacterizationFactor` One characterization factor matched against a database biosphere flow. @@ -990,6 +1053,12 @@ the slice is incomplete. | `shown` | `int` | — | | `factors` | `list[CharacterizationFactor]` | list() | +#### Properties + +##### `has_more` + +True when the server truncated below ``matches``. + ### `ClassificationFilter` Filter a supply-chain/consumers query by a classification (system, value, mode). @@ -1161,6 +1230,12 @@ response of :meth:`Client.get_flow_mapping`. | `matched_flows` | `int` | — | | `flows` | `list[FlowMappingEntry]` | list() | +#### Properties + +##### `coverage_pct` + +Matched fraction expressed as 0..100. Returns 0 when total is 0. + ### `FlowMappingEntry` One DB biosphere flow and the CF (if any) assigned to it. @@ -1407,6 +1482,12 @@ materialise eagerly if you prefer. | `_fetched` | `list[~T]` | list() | | `_exhausted` | `bool` | False | +#### Properties + +##### `page_size` + +Server-applied limit (page size for further fetches). + ### `ServerVersion` Server build metadata returned by :meth:`Client.get_version`. @@ -1462,6 +1543,16 @@ lengths by hand. | `entries` | `list[SupplyChainEntry]` | list() | | `edges` | `list[SupplyChainEdge]` | list() | +#### Properties + +##### `has_more` + +True when the server truncated ``entries`` below ``filtered_activities``. + +Surfacing this lets callers detect silent truncation: if you passed +``limit=100`` and ``filtered_activities`` is 500, downstream LCA work +would be wrong without flagging the gap. + ### `SupplyChainEdge` A consumer→supplier link in the supply chain. @@ -1519,6 +1610,22 @@ producing activity's classifications describe the product taxonomy. | `is_biosphere` | `bool` | False | | `is_waste` | `bool` | False | +#### Properties + +##### `is_input` + +True for technosphere inputs (``role`` is ``INPUT`` or ``REFERENCE_INPUT``). + +Lets callers split exchanges into inputs vs. outputs without +knowing the four-role taxonomy. + +##### `is_reference` + +True for reference roles (``REFERENCE_PRODUCT`` / ``REFERENCE_INPUT``). + +The reference exchange is the one that defines the activity's +functional unit — the basis the LCA result is normalised to. + ### `WasteExchange` An exchange of a waste flow with a treatment activity. @@ -1541,6 +1648,15 @@ product input. Orphan waste (no linked treatment) contributes zero impact | `is_biosphere` | `bool` | False | | `is_waste` | `bool` | True | +#### Properties + +##### `is_reference` + +Always False — waste flows never define an activity's functional unit. + +Treatment activities have a ``ReferenceInput`` instead, exposed +via :class:`TechnosphereExchange`. + ## Functions ### `compare_activities(client: Client, pid_left: str, pid_right: str, *, scope: str = 'direct', group_by: str = 'flow_id', is_input: bool | None = True, **aggregate_kwargs) -> ActivityDiff` diff --git a/pyvolca/scripts/gen_api_md.py b/pyvolca/scripts/gen_api_md.py index 4548ba03..c99db875 100644 --- a/pyvolca/scripts/gen_api_md.py +++ b/pyvolca/scripts/gen_api_md.py @@ -191,22 +191,29 @@ def render_dataclass(cls: type, buf: io.StringIO) -> None: if doc and not _is_autogenerated_dataclass_doc(cls, doc): buf.write(doc + "\n\n") fields = dataclasses.fields(cls) - if not fields: - return - buf.write("| Field | Type | Default |\n") - buf.write("|-------|------|---------|\n") - for f in fields: - # Escape pipes in type annotations (e.g. `str | None`) so they don't - # break markdown table cells. - type_str = _format_type(f.type).replace("|", "\\|") - if f.default is not dataclasses.MISSING: - default = repr(f.default) - elif f.default_factory is not dataclasses.MISSING: # type: ignore[misc] - default = f"{f.default_factory.__name__}()" # type: ignore[union-attr] - else: - default = "—" - buf.write(f"| `{f.name}` | `{type_str}` | {default} |\n") - buf.write("\n") + if fields: + buf.write("| Field | Type | Default |\n") + buf.write("|-------|------|---------|\n") + for f in fields: + # Escape pipes in type annotations (e.g. `str | None`) so they don't + # break markdown table cells. + type_str = _format_type(f.type).replace("|", "\\|") + if f.default is not dataclasses.MISSING: + default = repr(f.default) + elif f.default_factory is not dataclasses.MISSING: # type: ignore[misc] + default = f"{f.default_factory.__name__}()" # type: ignore[union-attr] + else: + default = "—" + buf.write(f"| `{f.name}` | `{type_str}` | {default} |\n") + buf.write("\n") + # Dataclasses carry computed @property members too (e.g. + # ``ActivityDetail.allocation_percent``); the field table alone hides them. + _, properties = _public_members(cls) + if properties: + buf.write("#### Properties\n\n") + for name, doc in properties: + buf.write(f"##### `{name}`\n\n") + buf.write((doc or "_(no docstring)_") + "\n\n") def render_function(fn: object, buf: io.StringIO) -> None: From 39a3ae815b81ab7bff6f3ae6f82f15cd83f5ab8d Mon Sep 17 00:00:00 2001 From: Christophe Combelles Date: Mon, 6 Jul 2026 19:08:49 +0200 Subject: [PATCH 2/2] Factor the Properties section into a shared helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit render_class and render_dataclass both emitted the identical Properties block byte-for-byte; a change to heading level or the no-docstring placeholder would have to land twice. Extract _render_properties and call it from both. Pure refactor — generated README is unchanged. --- pyvolca/scripts/gen_api_md.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pyvolca/scripts/gen_api_md.py b/pyvolca/scripts/gen_api_md.py index c99db875..66dd0353 100644 --- a/pyvolca/scripts/gen_api_md.py +++ b/pyvolca/scripts/gen_api_md.py @@ -149,6 +149,16 @@ def _public_members(cls: type) -> tuple[list[tuple[str, str, str]], list[tuple[s return methods, properties +def _render_properties(properties: list[tuple[str, str]], buf: io.StringIO) -> None: + """Append the Properties section — shared by class and dataclass rendering.""" + if not properties: + return + buf.write("#### Properties\n\n") + for name, doc in properties: + buf.write(f"##### `{name}`\n\n") + buf.write((doc or "_(no docstring)_") + "\n\n") + + def render_class(cls: type, buf: io.StringIO) -> None: """Render a class with its public methods. @@ -173,11 +183,7 @@ def render_class(cls: type, buf: io.StringIO) -> None: except (TypeError, ValueError): pass methods, properties = _public_members(cls) - if properties: - buf.write("#### Properties\n\n") - for name, doc in properties: - buf.write(f"##### `{name}`\n\n") - buf.write((doc or "_(no docstring)_") + "\n\n") + _render_properties(properties, buf) if methods: buf.write("#### Methods\n\n") for name, sig_str, doc in methods: @@ -209,11 +215,7 @@ def render_dataclass(cls: type, buf: io.StringIO) -> None: # Dataclasses carry computed @property members too (e.g. # ``ActivityDetail.allocation_percent``); the field table alone hides them. _, properties = _public_members(cls) - if properties: - buf.write("#### Properties\n\n") - for name, doc in properties: - buf.write(f"##### `{name}`\n\n") - buf.write((doc or "_(no docstring)_") + "\n\n") + _render_properties(properties, buf) def render_function(fn: object, buf: io.StringIO) -> None: