Links Notation Parser for Python
Python implementation of the Links Notation parser.
Installation
pip install links-notation
Quick Start
from links_notation import Parser
parser = Parser()
links = parser.parse("papa (lovesMama: loves mama)")
# Access parsed links
for link in links:
print(link)
Usage
Basic Parsing
from links_notation import Parser, format_links
parser = Parser()
# Parse simple links
links = parser.parse("(papa: loves mama)")
print(links[0].id) # 'papa'
print(len(links[0].values)) # 2
# Format links back to string
output = format_links(links)
print(output) # (papa: loves mama)
Working with Link Objects
from links_notation import Link
# Create links programmatically
link = Link('parent', [Link('child1'), Link('child2')])
print(str(link)) # (parent: child1 child2)
# Access link properties
print(link.id) # 'parent'
print(link.values[0].id) # 'child1'
# Combine links
combined = link.combine(Link('another'))
print(str(combined)) # ((parent: child1 child2) another)
Indented Syntax
parser = Parser()
# Parse indented notation
text = """3:
papa
loves
mama"""
links = parser.parse(text)
# Produces: (3: papa loves mama)
How a Document Was Written (CST)
Parsing keeps how every ID was written. Quoting never changes a reference:
1 and '1' are the same reference, so equality, str(), repr() and
format() stay as they were. Each parsed link also carries raw_id, the ID
exactly as written, and quote, the delimiter it was written between:
from links_notation import Parser
[link] = Parser().parse("(t 1 '1')")
_, bare, quoted = link.values
bare == quoted # True: the same reference
(bare.raw_id, bare.quote) # ('1', None)
(quoted.raw_id, quoted.quote) # ("'1'", "'")
link.format() # '(t 1 1)'
raw_id is None for links without an ID, links built in code, and links
made by indentation path combination. Use Parser(syntax=False) (AST mode) to
leave it out. reference_quote(raw) reads the delimiter of one written
reference.
parse_cst(input) returns the lossless concrete syntax tree. Every character,
including comments, blank lines, indentation, quotes and line endings, belongs
to exactly one token, so the original text is always recovered:
cst = Parser().parse_cst("(t 1 '1') # note\n")
str(cst) # "(t 1 '1') # note\n"
[(token.kind, token.text) for token in cst.tokens()]
# [('open_paren', '('), ('reference', 't'), ('whitespace', ' '), ...]
cst.to_s_expression()
# (document (line (group open_paren:"(" (line reference:"t" ...)
# close_paren:")") whitespace:" " comment:"# note" newline:"\n"))
A document node holds line nodes. A line holds its tokens up to and
including its line ending. A group node holds (, the lines inside it, and
). Reference tokens carry value (what the reference means) and quote.
parse_cst parses the document first, so it refuses what parse refuses,
and it also refuses unbalanced parentheses. See the
issue 364 case study.
Streaming Parsing
StreamParser accepts arbitrary chunks and emits only complete top-level
records. Set collect=False for bounded-memory callback or generator use.
from links_notation import StreamParser, parse_chunks
stream = StreamParser(on_link=print, collect=False)
stream.write("profile:\n name Ada\n")
stream.finish("next link")
for link in parse_chunks(["one li", "nk\ntwo link"]):
print(link)
parse_async_chunks provides the equivalent native async generator. The
stream also exposes position, drain(), reset(), and a buffer-size limit.
API Reference
Parser
The main parser class for Links Notation.
__init__(max_input_size: int = 10 * 1024 * 1024, max_depth: int = DEFAULT_MAX_DEPTH, comments: bool = True, syntax: bool = True): Create a parser; withcomments=Falsea#is an ordinary character instead of the start of a comment; withsyntax=False(AST mode) parsed links leaveraw_idandquoteasNoneparse(input_text: str) -> List[Link]: Parse Links Notation text into Link objects; raisesParseErrorwhen the text does not parse or nests links deeper thanmax_depthparse_cst(input_text: str) -> CstNode: Read the lossless concrete syntax tree;str()of it is the inputDEFAULT_MAX_DEPTH: The defaultmax_depth, 64, the same in every implementation
max_depth is how deep links may nest (default: 64). Every parenthesized group
and every indentation level is one level, and the lines of a document start at
level 0, so with max_depth=1 (a) is accepted while ((a)), (a (b)) and a
group on an indented line are refused. A document nested deeper is refused with
a ParseError rather than recursed into until Python's recursion limit is hit.
ParseError
Raised when parsing fails. When a document nests links deeper than
max_depth, it points at the group or the line that is one level too deep:
Nesting too deep at line 1, column 4: nesting depth exceeds the maximum of 3
1 | ((((a))))
| ^
max_depth: The deepest nesting the parser accepts, when the document nests deeper;Nonefor any other errorline,column: Where the offending group or line starts, counted from 1offset: The same position as a character offset from the start of the documentline_text: The offending line, as written
StreamParser reports the same error as a StreamParseError whose error is
the ParseError and whose line, column and offset are counted from the
start of the stream.
Link
Represents a link in Links Notation.
__init__(id: Optional[str] = None, values: Optional[List[Link]] = None)format(less_parentheses: bool = False) -> str: Format as stringsimplify() -> Link: Simplify link structurecombine(other: Link) -> Link: Combine with another linkraw_id: The ID exactly as the parsed document wrote it, orNone; ignored by==,str(),repr()andformat()quote:',"or`whenraw_idis quoted, otherwiseNone
format_links
Format a list of links into Links Notation.
format_links(links: List[Link], less_parentheses: bool = False) -> str
Examples
Doublets (2-tuple)
parser = Parser()
text = """
papa (lovesMama: loves mama)
son lovesMama
daughter lovesMama
"""
links = parser.parse(text)
Triplets (3-tuple)
text = """
papa has car
mama has house
(papa and mama) are happy
"""
links = parser.parse(text)
Quoted References
# References with special characters need quotes
text = '("has space": "value with: colon")'
links = parser.parse(text)
Multi-line Groups
A parenthesized group opens a nested context: its body starts fresh at indentation level zero and follows the same rules as the root document, so a line break inside parentheses is structure rather than decoration.
value (
id "1"
label "one"
)
The document above parses to (value ((id 1) (label one))) - two children, each
a link of its own - rather than to one flat list in which the boundary between
id and label would be lost. A body that stays on a single line still
collapses to a single link, so (a b c) is unchanged.
text = """value (
id "1"
label "one"
)"""
print(format_links(parser.parse(text)))
# (value ((id 1) (label one)))
Comments
A # hides the rest of the line it stands on, so a document can carry prose
about itself:
# the machines this deploys to
deploy: staging # only staging, for now
Both comments are gone by the time the document is read, leaving the single
link (deploy: staging). A # only opens a comment where a reference could
begin, so a # inside a token (issue#1047) and a # inside a delimited
reference ("#") stay ordinary characters.
A formatter keeps the same rule from the other side: a reference that begins
with a # is written quoted ('#tag'), so a document it writes reads back as
itself.
Comments are on by default, and a parser can be told to read # as an ordinary
character again, for documents written before comments existed:
document = "# the machines this deploys to\ndeploy: staging # only staging, for now\n"
print(format_links(Parser().parse(document))) # (deploy: staging)
print(format_links(Parser(comments=False).parse("# a b\n"))) # (# a b)
Development
Running Tests
# Install development dependencies
pip install pytest
# Run tests
pytest
Building
pip install build
python -m build
Maintenance
Linting and Formatting
This project uses Black for code formatting, isort for import sorting, and flake8 for linting.
Install linting tools:
pip install ".[lint]"
# Or install all dev dependencies
pip install ".[dev]"
Format all code files
black .
isort .
Check formatting (without modifying files)
black --check --diff .
isort --check-only --diff .
flake8 --max-line-length=120
These checks are enforced in CI. Pull requests with unformatted code will fail the lint check.
Pre-commit Hooks
This project uses pre-commit hooks that automatically run Black, isort, and flake8 before commits. To set up pre-commit hooks locally:
# From repository root
pip install pre-commit
pre-commit install
License
This project is released into the public domain under the Unlicense.
Links
Exact Unicode identifiers and values round-trip with the reference literal contract introduced in 0.24.0, including mixed quotes, empty strings, controls and multiline text.
Metadata
Release files for links-notation 0.30.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| links_notation-0.30.0.tar.gz | 66.4 kB | Without provenance | Details for links_notation-0.30.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| links_notation-0.30.0-py3-none-any.whl | Python 3 | none | any | Details for links_notation-0.30.0-py3-none-any.whl |
Total release size: 104.3 kB
Release files / links_notation-0.30.0.tar.gz
| Download URL | links_notation-0.30.0.tar.gz |
|---|---|
| Size | 66.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a3821802ba987e9b0947d78a089130b40a46945e935305aedba0d275049e7895
|
|
BLAKE2b-256 checksum How to use checksums |
909a85fecc34af5549f3ff894198ca25091a47ff667b7e659a497831c1783b72
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Release files / links_notation-0.30.0-py3-none-any.whl
| Download URL | links_notation-0.30.0-py3-none-any.whl |
|---|---|
| Size | 38.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0a60690b5db2ec43fe272345617deb7dde93f064eb6244941adbb6adeecb59d0
|
|
BLAKE2b-256 checksum How to use checksums |
c62923696a580c33783e5789026f1ddd1db422a7587b7fe412893b7db864948d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Security for links-notation 0.30.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.