fixeria has uploaded this change for review.

View Change

docs: auto-generate Card Filesystem Reference

Add a Sphinx extension (docs/pysim_fs_sphinx.py) that hooks into the
builder-inited event and generates docs/filesystem.rst before Sphinx
reads any source files.

The generated page contains a hierarchical listing of all implemented
EFs and DFs, organised by application/specification (UICC/TS 102 221,
ADF.USIM/TS 31.102, ADF.ISIM/TS 31.103, SIM/TS 51.011). For each file,
the class docstring and any _test_de_encode / _test_decode vectors
are included as an encoding/decoding example table.

docs/filesystem.rst is fully generated at build time and is therefore
added to .gitignore.

Change-Id: I06ddeefc6c11e04d7c24e116f3f39c8a6635856f
Related: OS#6316
---
M .gitignore
M docs/conf.py
M docs/index.rst
A docs/pysim_fs_sphinx.py
4 files changed, 253 insertions(+), 1 deletion(-)

git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/51/42551/1
diff --git a/.gitignore b/.gitignore
index 0cd5b14..3f1f645 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@

/docs/_*
/docs/generated
+/docs/filesystem.rst
/.cache
/.local
/build
diff --git a/docs/conf.py b/docs/conf.py
index 165d235..2a6bbd3 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -13,6 +13,7 @@
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
+sys.path.insert(0, os.path.abspath('.')) # for local extensions (pysim_fs_sphinx, …)


# -- Project information -----------------------------------------------------
@@ -39,7 +40,8 @@
"sphinx.ext.autodoc",
"sphinxarg.ext",
"sphinx.ext.autosectionlabel",
- "sphinx.ext.napoleon"
+ "sphinx.ext.napoleon",
+ "pysim_fs_sphinx",
]

# Add any paths that contain templates here, relative to this directory.
@@ -81,8 +83,13 @@
# reader", "decode_hex", etc.) still slip through. Suppress the residual
# warnings per-file; cross-references into these sections are not a supported
# use-case.
+#
+# filesystem.rst is auto-generated and intentionally repeats DF names across
+# different application sections (e.g. DF.PHONEBOOK in both ADF.USIM and
+# DF.GSM); suppress its duplicate-label warnings for the same reason.
autosectionlabel_maxdepth = 3
suppress_warnings = [
+ 'autosectionlabel.filesystem',
'autosectionlabel.saip-tool',
'autosectionlabel.shell',
'autosectionlabel.smpp2sim',
diff --git a/docs/index.rst b/docs/index.rst
index a6ed7b9..8908c4e 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -39,6 +39,7 @@
:caption: Contents:

shell
+ filesystem
trace
legacy
smpp2sim
diff --git a/docs/pysim_fs_sphinx.py b/docs/pysim_fs_sphinx.py
new file mode 100644
index 0000000..37f3577
--- /dev/null
+++ b/docs/pysim_fs_sphinx.py
@@ -0,0 +1,243 @@
+"""
+Sphinx extension: auto-generate docs/filesystem.rst from the pySim EF class hierarchy.
+
+Hooked into Sphinx's ``builder-inited`` event so the file is always regenerated
+from the live Python classes before Sphinx reads any source files.
+
+The table of root objects to document is in SECTIONS near the top of this file.
+"""
+
+import importlib
+import inspect
+import json
+import os
+import sys
+import textwrap
+
+# Ensure pySim is importable when this module is loaded as a Sphinx extension
+sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
+
+from pySim.filesystem import (CardApplication, CardDF, CardMF, CardEF, # noqa: E402
+ TransparentEF, TransRecEF, LinFixedEF, CyclicEF, BerTlvEF)
+from pySim.profile import CardProfile # noqa: E402
+
+
+# Generic EF base classes whose docstrings describe the *type* of file
+# (Transparent, LinFixed, ...) rather than a specific file's content.
+# Suppress those boilerplate texts in the per-EF entries; they are only
+# useful once, at the top of the document or in a dedicated glossary.
+_EF_BASE_TYPES = frozenset([TransparentEF,
+ TransRecEF,
+ LinFixedEF,
+ CyclicEF,
+ BerTlvEF])
+
+
+# ---------------------------------------------------------------------------
+# Sections: (heading, module, class-name)
+# The class must be either a CardProfile (uses .files_in_mf) or a CardDF
+# subclass (uses .children).
+# ---------------------------------------------------------------------------
+SECTIONS = [
+ ('MF / TS 102 221 (UICC)',
+ 'pySim.ts_102_221', 'CardProfileUICC'),
+ ('ADF.USIM / TS 31.102',
+ 'pySim.ts_31_102', 'ADF_USIM'),
+ ('ADF.ISIM / TS 31.103',
+ 'pySim.ts_31_103', 'ADF_ISIM'),
+ ('ADF.HPSIM / TS 31.104',
+ 'pySim.ts_31_104', 'ADF_HPSIM'),
+ ('DF.GSM + DF.TELECOM / TS 51.011 (SIM)',
+ 'pySim.ts_51_011', 'CardProfileSIM'),
+ ('CDMA / IS-820 (RUIM)',
+ 'pySim.cdma_ruim', 'CardProfileRUIM'),
+ ('DF.EIRENE / GSM-R',
+ 'pySim.gsm_r', 'DF_EIRENE'),
+ ('DF.SYSTEM / sysmocom SJA2+SJA5',
+ 'pySim.sysmocom_sja2', 'DF_SYSTEM'),
+ # pySim.ara_m / ADF_ARAM: ARA-M has no EF/DF children defined in pySim.
+ # pySim.euicc / CardProfileEuiccSGP{02,22,32}: no MF-level files beyond base UICC.
+ # pySim.euicc / CardApplicationISDR, CardApplicationECASD: no EF/DF children.
+ # pySim.ts_102_310 / DF_EAP: skipped due to a NameError bug in the module (EF_EAPKEYS
+ # self-references its own class name during class body evaluation).
+]
+
+# RST underline characters ordered by nesting depth
+_HEADING_CHARS = ['=', '=', '-', '~', '^', '"']
+# Level 0 uses '=' with overline (page title).
+# Level 1 uses '=' without overline (major sections).
+# Levels 2+ use the remaining characters for DFs.
+
+
+# ---------------------------------------------------------------------------
+# RST formatting helpers
+# ---------------------------------------------------------------------------
+
+def _heading(title: str, level: int) -> str:
+ """Return an RST heading string. Level 0 gets an overline."""
+ char = _HEADING_CHARS[level]
+ rule = char * len(title)
+ if level == 0:
+ return f'{rule}\n{title}\n{rule}\n\n'
+ return f'{title}\n{rule}\n\n'
+
+
+def _json_default(obj):
+ """Fallback serialiser: bytes -> hex, anything else -> repr."""
+ if isinstance(obj, (bytes, bytearray)):
+ return obj.hex()
+ return repr(obj)
+
+
+def _examples_block(cls) -> str:
+ """Return RST code-block examples (one per vector), or '' if none exist.
+
+ Each example is rendered as a ``json5`` code-block with the hex-encoded
+ binary as a ``// comment`` on the first line, followed by the decoded JSON.
+ ``json5`` is used instead of ``json`` so that Pygments does not flag the
+ ``//`` comment as a syntax error.
+ """
+ vectors = []
+ for attr in ('_test_de_encode', '_test_decode'):
+ v = getattr(cls, attr, None)
+ if v:
+ vectors.extend(v)
+ if not vectors:
+ return ''
+
+ lines = ['**Examples**\n\n']
+
+ for t in vectors:
+ # 2-tuple: (encoded, decoded)
+ # 3-tuple: (encoded, record_nr, decoded) — LinFixedEF
+ encoded = t[0]
+ decoded = t[2] if len(t) >= 3 else t[1]
+
+ json_str = json.dumps(decoded, default=_json_default, indent=2)
+ json_indented = textwrap.indent(json_str, ' ')
+
+ lines.append('.. code-block:: json5\n\n')
+ lines.append(f' // {encoded.lower()}\n')
+ lines.append(json_indented + '\n')
+ lines.append('\n')
+
+ return ''.join(lines)
+
+
+def _document_ef(ef: CardEF) -> str:
+ """Return RST for a single EF. Uses ``rubric`` to stay out of the TOC."""
+ cls = type(ef)
+
+ parts = [ef.fully_qualified_path_str()]
+ if ef.fid:
+ parts.append(f'({ef.fid.upper()})')
+ if ef.desc:
+ parts.append(f'\u2014 {ef.desc}') # em-dash
+ title = ' '.join(parts)
+
+ lines = [f'.. rubric:: {title}\n\n']
+
+ # Only show a docstring if it is specific to this class. EFs that are
+ # direct instances of a base type (TransparentEF, LinFixedEF, …) carry
+ # only the generic "what is a TransparentEF" boilerplate; named subclasses
+ # without their own __doc__ have cls.__dict__['__doc__'] == None. Either
+ # way, suppress the text here — it belongs at the document level, not
+ # repeated for every single EF entry.
+ doc = None if cls in _EF_BASE_TYPES else cls.__dict__.get('__doc__')
+ if doc:
+ lines.append(inspect.cleandoc(doc) + '\n\n')
+
+ examples = _examples_block(cls)
+ if examples:
+ lines.append(examples)
+
+ return ''.join(lines)
+
+
+def _document_df(df: CardDF, level: int) -> str:
+ """Return RST for a DF section and all its children, recursively."""
+ parts = [df.fully_qualified_path_str()]
+ if df.fid:
+ parts.append(f'({df.fid.upper()})')
+ if df.desc:
+ parts.append(f'\u2014 {df.desc}') # em-dash
+ title = ' '.join(parts)
+
+ lines = [_heading(title, level)]
+
+ cls = type(df)
+ doc = None if cls in (CardDF, CardMF) else cls.__dict__.get('__doc__')
+ if doc:
+ lines.append(inspect.cleandoc(doc) + '\n\n')
+
+ for child in df.children.values():
+ if isinstance(child, CardDF):
+ lines.append(_document_df(child, level + 1))
+ elif isinstance(child, CardEF):
+ lines.append(_document_ef(child))
+
+ return ''.join(lines)
+
+
+# ---------------------------------------------------------------------------
+# Top-level generator
+# ---------------------------------------------------------------------------
+
+def generate_filesystem_rst() -> str:
+ """Walk all registered sections and return the full RST document as a string."""
+ out = [
+ '.. This file is auto-generated by docs/pysim_fs_sphinx.py — do not edit.\n\n',
+ _heading('Card Filesystem Reference', 0),
+ 'This page documents all Elementary Files (EFs) and Dedicated Files (DFs) '
+ 'implemented in pySim, organised by their location in the card filesystem.\n\n',
+ ]
+
+ # Track already-documented classes so that DFs/EFs shared between profiles
+ # (e.g. DF.TELECOM / DF.GSM present in both CardProfileSIM and CardProfileRUIM)
+ # are only emitted once.
+ seen_types: set = set()
+
+ for section_title, module_path, class_name in SECTIONS:
+ module = importlib.import_module(module_path)
+ cls = getattr(module, class_name)
+ obj = cls()
+
+ if isinstance(obj, CardProfile):
+ files = obj.files_in_mf
+ elif isinstance(obj, CardApplication):
+ files = list(obj.adf.children.values())
+ elif isinstance(obj, CardDF):
+ files = list(obj.children.values())
+ else:
+ continue
+
+ # Filter out files whose class was already documented in an earlier section.
+ files = [f for f in files if type(f) not in seen_types]
+ if not files:
+ continue
+
+ out.append(_heading(section_title, 1))
+
+ for f in files:
+ seen_types.add(type(f))
+ if isinstance(f, CardDF):
+ out.append(_document_df(f, level=2))
+ elif isinstance(f, CardEF):
+ out.append(_document_ef(f))
+
+ return ''.join(out)
+
+
+# ---------------------------------------------------------------------------
+# Sphinx integration
+# ---------------------------------------------------------------------------
+
+def _on_builder_inited(app):
+ output_path = os.path.join(app.srcdir, 'filesystem.rst')
+ with open(output_path, 'w') as fh:
+ fh.write(generate_filesystem_rst())
+
+
+def setup(app):
+ app.connect('builder-inited', _on_builder_inited)
+ return {'version': '0.1', 'parallel_read_safe': True}

To view, visit change 42551. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-MessageType: newchange
Gerrit-Project: pysim
Gerrit-Branch: master
Gerrit-Change-Id: I06ddeefc6c11e04d7c24e116f3f39c8a6635856f
Gerrit-Change-Number: 42551
Gerrit-PatchSet: 1
Gerrit-Owner: fixeria <vyanitskiy@sysmocom.de>