DocName: The Canonical creators__year__title Document Name#

class my.files.DocName.DocName(*, creators: str, year: str, title: str)#

One document’s canonical creators__year__title name.

The model is frozen and validates its own grammar, so a DocName that exists is a name that can be written to disk. Build one with mint() from bibliographic metadata, or with parse() from a name that already exists.

Examples

Mint a name from what a document’s front matter yielded:

>>> from my.files import DocName
>>> str(DocName.mint(creators=['Hannah Arendt'], year=1958,
...                  title='The Human Condition'))
'arendt-h__1958__the-human-condition'

Fall through to the identifier when the creators could not be read:

>>> str(DocName.mint(title='A Survey of Agent Memory', year=2025,
...                  uids={'arxiv': '2501.13956'}))
'uid-arxiv__2025__a-survey-of-agent-memory'

Parse a name back into its slots:

>>> DocName.parse('aristotle__-0350__categories').year
'-0350'

Building a name#

classmethod DocName.mint(title: str = '', creators: Iterable[str] = (), year: int | str | None = None, uids: dict[str, str] | None = None, site: str = '', et_al: bool = False) → Self | None#

Build the best canonical name the given metadata can support.

Walks the evidence ladder, best first: named creators, then the work’s own identifier, then whoever published it. Each rung yields a name a reader can find again; only a work with no title and no identifier at all is unnameable.

Parameters:
  • title – The work’s title, as written.

  • creators – Creator names, as written, most significant first.

  • year – Publication year, 'undated', or None when untriaged.

  • uids – Identifiers by scheme, e.g. {'arxiv': '2501.13956'}.

  • site – Publisher, website, or bare domain, for an author-less work.

  • et_al – Force the et-al sub-unit even when creators was complete.

Returns:

The minted name, or None when the metadata names nothing.

classmethod DocName.parse(text: str) → Self | None#

Read an existing document name, tolerating a path and a file extension.

Parameters:

text – A name, filename, or directory name.

Returns:

The parsed name, or None when text does not obey the grammar.

static DocName.is_valid(text: str) → bool#

Whether text obeys the typed-slot grammar and creator delineation.

Parameters:

text – A candidate name.

Returns:

Whether the shelf would accept it.

Examples

The et-al sub-unit must be delineated, not hyphenated on:

>>> from my.files import DocName
>>> DocName.is_valid('cuneo-n_et-al__2005__the-normative-web')
True
>>> DocName.is_valid('cuneo-n-et-al__2005__the-normative-web')
False

Using a name#

DocName.filename(suffix: str = '') → str#

This name as a filename.

Parameters:

suffix – A file extension, with or without its leading dot.

Returns:

The name, with suffix appended when one was given.

DocName.disambiguated(taken: Container[str], suffix: str = '') → Self#

A variant of this name that taken does not already hold.

Two different works can mint one name – a second edition, a reprint, two papers a year apart with one title. The shelf must never silently absorb one into the other, so the copy takes an ordinal sub-unit on its title slot: syntactic-structures becomes syntactic-structures_2.

Parameters:
  • taken – The names already shelved. Membership is tested against filename(), so pass the same shape you store.

  • suffix – The file extension taken is keyed by, if any.

Returns:

This name when it is free, else the first free ordinal variant.

Slots#

my.files.DocName.creators_slot(names: Iterable[str], et_al: bool = False, limit: int = 2) → str#

The creator slot for names: up to limit people, _-joined, then _et-al.

Parameters:
  • names – Creator names, as written, most significant first.

  • et_al – Force the et-al sub-unit even when names was already complete.

  • limit – How many named creators the slot holds before the rest collapse.

Returns:

The creator slot, or '' when no name survives cleaning.

Examples

Two creators join with _; a third collapses into et-al:

>>> from my.files import DocName
>>> DocName.creators_slot(['Tamar Gendler', 'John Hawthorne'])
'gendler-t_hawthorne-j'
>>> DocName.creators_slot(['Noam Chomsky', 'Robert Pollin', 'C. J. Polychroniou'])
'chomsky-n_pollin-r_et-al'
my.files.DocName.person_slot(name: str) → str#

One creator as surname-initials, or a bare surname when no given name is known.

Parameters:

name – One person’s name, as written.

Returns:

The sub-unit for this person, or '' when no surname survives cleaning.

Examples

Name a person in either written order:

>>> from my.files import DocName
>>> DocName.person_slot('Hannah Arendt')
'arendt-h'
>>> DocName.person_slot('Austin, J. L.')
'austin-jl'
>>> DocName.person_slot('Aristotle')
'aristotle'
my.files.DocName.split_person(name: str) → tuple[str, str]#

Split one written name into (surname, given).

Reads both Firstname Lastname and Lastname, Firstname orders.

A particle’s own case decides whether it belongs to the surname, which is the bibliographic rule and the one the shelf follows: a lowercase particle is detachable and drops out of the sort form (Simone de Beauvoir files under beauvoir-s), while a capitalized one is part of the surname proper (Helen De Cruz stays de-cruz-h).

Parameters:

name – One person’s name, as written.

Returns:

The surname and given-name halves, either of which may be ''. A detached lowercase particle appears in neither – it is not part of the sort form.

my.files.DocName.year_slot(value: int | str | None) → str#

The year slot for value: signed and zero-padded to four digits.

Parameters:

value – A year as an integer or string, the literal 'undated', or None.

Returns:

A signed four-digit year, 'undated', or '0000' when nothing was readable.

Examples

Pad, sign, and pass through the two non-numeric answers:

>>> from my.files import DocName
>>> DocName.year_slot(1958), DocName.year_slot(-350), DocName.year_slot(950)
('1958', '-0350', '0950')
>>> DocName.year_slot(None), DocName.year_slot('undated')
('0000', 'undated')
my.files.DocName.title_slot(title: str, max_length: int = 48) → str#

The title slot for title, cut at its first subtitle mark then cleaned.

Parameters:
  • title – The work’s title, as written.

  • max_length – Length cap, in characters.

Returns:

The cleaned title slot, or '' when nothing survives.

Examples

A subtitle is dropped at the mark that introduces it:

>>> from my.files import DocName
>>> DocName.title_slot('Steps to an Ecology of Mind: Collected Essays')
'steps-to-an-ecology-of-mind'
my.files.DocName.uid_slot(scheme: str, value: str) → str#

The creator-slot stand-in for a work identified by scheme, e.g. uid-arxiv.

Parameters:
  • scheme – The identifier’s namespace (arxiv, doi, isbn, …).

  • value – The identifier itself. Only used to prove one exists.

Returns:

The uid-<scheme> sub-unit, or '' when neither half survives cleaning.

my.files.DocName.uid_handle(value: str) → str#

One identifier as a slot sub-unit, keeping its internal structure as -.

Parameters:

value – The identifier, as written.

Returns:

The cleaned handle, or '' when nothing survives.

Examples

An arXiv id and a DOI keep their shape:

>>> from my.files import DocName
>>> DocName.uid_handle('2501.13956')
'2501-13956'
>>> DocName.uid_handle('10.1145/3597503')
'10-1145-3597503'
my.files.DocName.site_slot(site: str) → str#

The creator-slot stand-in for an author-less work, named by who published it.

Parameters:

site – A publisher, website, or bare domain.

Returns:

The site-<host> sub-unit, or '' when nothing survives cleaning.

Examples

A domain keeps its labels; a publisher name is cleaned like any other text:

>>> from my.files import DocName
>>> DocName.site_slot('https://example.org/news/x')
'site-example-org'
>>> DocName.site_slot('Cambridge University Press')
'site-cambridge-university-press'

Cleaning#

my.files.DocName.clean_section(text: str, max_length: int = 48) → str#

Clean one slot’s text and cut it back to the last whole word that fits.

Never drops an internal article or preposition – those are title identity. Only a stopword left dangling by the cut is removed, because it names nothing on its own.

Punctuation that separated two phrases becomes the _ sub-unit delimiter, so a slot keeps the shape its source had. Cutting a title at its subtitle mark is a different act, and belongs to title_slot().

Parameters:
  • text – Raw slot text.

  • max_length – Length cap, in characters.

Returns:

The cleaned, truncated slot, or '' when nothing survives.

Examples

Clean and cap a title, cutting back to a whole word:

>>> from my.files import DocName
>>> DocName.clean_section('The Structure of Scientific Revolutions')
'the-structure-of-scientific-revolutions'
>>> DocName.clean_section('Design for a Brain the Origin of Adaptive', 30)
'design-for-a-brain-the-origin'

Punctuation between phrases delimits sub-units:

>>> DocName.clean_section('Design for a Brain: The Origin of Adaptive', 30)
'design-for-a-brain_the-origin'
my.files.DocName.uid_clean(text: str) → str#

Strip identifier detritus (dots, quotes, commas) and surrounding quote marks.

my.files.DocName.without_leading_article(title: str) → str#

Remove one identity-neutral leading article, preserving every internal word.

Examples

Only the leading article goes; internal ones are the title’s identity:

>>> from my.files import DocName
>>> DocName.without_leading_article('the-human-condition')
'human-condition'
>>> DocName.without_leading_article('being-and-time')
'being-and-time'