SF.net SVN: docutils:[9946] trunk/docutils

aa-turner--- via Docutils-checkins <[email protected]>
Newsgroups gmane.text.docutils.cvs
Message-ID <[email protected]>
Revision: 9946
          http://sourceforge.net/p/docutils/code/9946
Author:   aa-turner
Date:     2024-10-13 00:24:58 +0000 (Sun, 13 Oct 2024)
Log Message:
-----------
Use UNIX-style (LF) line endings

Modified Paths:
--------------
    trunk/docutils/docs/ref/rst/history.rst
    trunk/docutils/docutils/utils/_roman_numerals.py
    trunk/docutils/licenses/BSD-0-Clause.rst
    trunk/docutils/test/test_utils/test__roman_numerals.py

Modified: trunk/docutils/docs/ref/rst/history.rst
===================================================================
--- trunk/docutils/docs/ref/rst/history.rst	2024-10-04 12:21:52 UTC (rev 9945)
+++ trunk/docutils/docs/ref/rst/history.rst	2024-10-13 00:24:58 UTC (rev 9946)
@@ -1,167 +1,167 @@
-.. include:: ../../header2.rst
-
-=============================
- History of reStructuredText
-=============================
-:Author: David Goodger
-:Contact: [email protected]
-:Revision: $Revision$
-:Date: $Date$
-:Copyright: This document has been placed in the public domain.
-
-reStructuredText_, the specification, is based on StructuredText_ and
-Setext_.  StructuredText was developed by Jim Fulton of `Zope
-Corporation`_ (formerly Digital Creations) and first released in 1996.
-It is now released as a part of the open-source "Z Object Publishing
-Environment" (ZOPE_).  Ian Feldman's and Tony Sanders' earlier Setext_
-specification was either an influence on StructuredText or, by their
-similarities, at least evidence of the correctness of this approach.
-
-I discovered StructuredText_ in late 1999 while searching for a way to
-document the Python modules in one of my projects.  Version 1.1 of
-StructuredText was included in Daniel Larsson's pythondoc_.  Although
-I was not able to get pythondoc to work for me, I found StructuredText
-to be almost ideal for my needs.  I joined the Python Doc-SIG_
-(Documentation Special Interest Group) mailing list and found an
-ongoing discussion of the shortcomings of the StructuredText
-"standard".  This discussion has been going on since the inception of
-the mailing list in 1996, and possibly predates it.
-
-I decided to modify the original module with my own extensions and
-some suggested by the Doc-SIG members.  I soon realized that the
-module was not written with extension in mind, so I embarked upon a
-general reworking, including adapting it to the "re" regular
-expression module (the original inspiration for the name of this
-project).  Soon after I completed the modifications, I discovered that
-StructuredText.py was up to version 1.23 in the ZOPE distribution.
-Implementing the new syntax extensions from version 1.23 proved to be
-an exercise in frustration, as the complexity of the module had become
-overwhelming.
-
-In 2000, development on StructuredTextNG ("Next Generation") began at
-`Zope Corporation`_ (then Digital Creations).  It seems to have many
-improvements, but still suffers from many of the problems of classic
-StructuredText.
-
-I decided that a complete rewrite was in order, and even started a
-`reStructuredText SourceForge project`_ (now inactive).  My
-motivations (the "itches" I aim to "scratch") are as follows:
-
-- I need a standard format for inline documentation of the programs I
-  write.  This inline documentation has to be convertible to other
-  useful formats, such as HTML.  I believe many others have the same
-  need.
-
-- I believe in the Setext/StructuredText idea and want to help
-  formalize the standard.  However, I feel the current specifications
-  and implementations have flaws that desperately need fixing.
-
-- reStructuredText could form part of the foundation for a
-  documentation extraction and processing system, greatly benefitting
-  Python.  But it is only a part, not the whole.  reStructuredText is
-  a markup language specification and a reference parser
-  implementation, but it does not aspire to be the entire system.  I
-  don't want reStructuredText or a hypothetical Python documentation
-  processor to die stillborn because of over-ambition.
-
-- Most of all, I want to help ease the documentation chore, the bane
-  of many a programmer.
-
-Unfortunately I was sidetracked and stopped working on this project.
-In November 2000 I made the time to enumerate the problems of
-StructuredText and possible solutions, and complete the first draft of
-a specification.  This first draft was posted to the Doc-SIG in three
-parts:
-
-- `A Plan for Structured Text`__
-- `Problems With StructuredText`__
-- `reStructuredText: Revised Structured Text Specification`__
-
-__ https://mail.python.org/pipermail/doc-sig/2000-November/001239.html
-__ https://mail.python.org/pipermail/doc-sig/2000-November/001240.html
-__ https://mail.python.org/pipermail/doc-sig/2000-November/001241.html
-
-In March 2001 a flurry of activity on the Doc-SIG spurred me to
-further revise and refine my specification, the result of which you
-are now reading.  An offshoot of the reStructuredText project has been
-the realization that a single markup scheme, no matter how well
-thought out, may not be enough.  In order to tame the endless debates
-on Doc-SIG, a flexible `Docstring Processing System framework`_ needed
-to be constructed.  This framework has become the more important of
-the two projects; reStructuredText_ has found its place as one
-possible choice for a single component of the larger framework.
-
-The project web site and the first project release were rolled out in
-June 2001, including posting the second draft of the spec [#spec-2]_
-and the first draft of PEPs 256, 257, and 258 [#peps-1]_ to the
-Doc-SIG.  These documents and the project implementation proceeded to
-evolve at a rapid pace.  Implementation history details can be found
-in the `project history file`_.
-
-In November 2001, the reStructuredText parser was nearing completion.
-Development of the parser continued with the addition of small
-convenience features, improvements to the syntax, the filling in of
-gaps, and bug fixes.  After a long holiday break, in early 2002 most
-development moved over to the other Docutils components, the
-"Readers", "Writers", and "Transforms".  A "standalone" reader
-(processes standalone text file documents) was completed in February,
-and a basic HTML writer (producing HTML 4.01, using CSS-1) was
-completed in early March.
-
-`PEP 287`_, "reStructuredText Standard Docstring Format", was created
-to formally propose reStructuredText as a standard format for Python
-docstrings, PEPs, and other files.  It was first posted to
-comp.lang.python_ and the Python-dev_ mailing list on 2002-04-02.
-
-Version 0.4 of the reStructuredText__ and `Docstring Processing
-System`_ projects were released in April 2002.  The two projects were
-immediately merged, renamed to "Docutils_", and a 0.1 release soon
-followed.
-
-.. __: `reStructuredText SourceForge project`_
-
-.. [#spec-2] The second draft of the spec:
-
-   - `An Introduction to reStructuredText`__
-   - `Problems With StructuredText`__
-   - `reStructuredText Markup Specification`__
-   - `Python Extensions to the reStructuredText Markup
-     Specification`__
-
-   __ https://mail.python.org/pipermail/doc-sig/2001-June/001858.html
-   __ https://mail.python.org/pipermail/doc-sig/2001-June/001859.html
-   __ https://mail.python.org/pipermail/doc-sig/2001-June/001860.html
-   __ https://mail.python.org/pipermail/doc-sig/2001-June/001861.html
-
-.. [#peps-1] First drafts of the PEPs:
-
-   - `PEP 256: Docstring Processing System Framework`__
-   - `PEP 258: DPS Generic Implementation Details`__
-   - `PEP 257: Docstring Conventions`__
-
-   Current working versions of the PEPs can be found in
-   https://docutils.sourceforge.io/docs/peps/, and official versions
-   can be found in the `master PEP repository`_.
-
-   __ https://mail.python.org/pipermail/doc-sig/2001-June/001855.html
-   __ https://mail.python.org/pipermail/doc-sig/2001-June/001856.html
-   __ https://mail.python.org/pipermail/doc-sig/2001-June/001857.html
-
-
-.. _reStructuredText: https://docutils.sourceforge.io/rst.html
-.. _StructuredText: https://zopestructuredtext.readthedocs.org/
-.. _Setext: https://docutils.sourceforge.io/mirror/setext.html
-.. _Docutils: https://docutils.sourceforge.io/
-.. _Doc-SIG: https://www.python.org/sigs/doc-sig/
-.. _Zope Corporation: http://www.zope.com
-.. _ZOPE: https://www.zope.dev
-.. _reStructuredText SourceForge project:
-   http://structuredtext.sourceforge.net/
-.. _pythondoc: http://starship.python.net/crew/danilo/pythondoc/
-.. _project history file: ../../../HISTORY.html
-.. _PEP 287: ../../peps/pep-0287.html
-.. _Docstring Processing System framework: ../../peps/pep-0256.html
-.. _comp.lang.python: news:comp.lang.python
-.. _Python-dev: https://mail.python.org/pipermail/python-dev/
-.. _Docstring Processing System: http://docstring.sourceforge.net/
-.. _master PEP repository: https://peps.python.org/
+.. include:: ../../header2.rst
+
+=============================
+ History of reStructuredText
+=============================
+:Author: David Goodger
+:Contact: [email protected]
+:Revision: $Revision$
+:Date: $Date$
+:Copyright: This document has been placed in the public domain.
+
+reStructuredText_, the specification, is based on StructuredText_ and
+Setext_.  StructuredText was developed by Jim Fulton of `Zope
+Corporation`_ (formerly Digital Creations) and first released in 1996.
+It is now released as a part of the open-source "Z Object Publishing
+Environment" (ZOPE_).  Ian Feldman's and Tony Sanders' earlier Setext_
+specification was either an influence on StructuredText or, by their
+similarities, at least evidence of the correctness of this approach.
+
+I discovered StructuredText_ in late 1999 while searching for a way to
+document the Python modules in one of my projects.  Version 1.1 of
+StructuredText was included in Daniel Larsson's pythondoc_.  Although
+I was not able to get pythondoc to work for me, I found StructuredText
+to be almost ideal for my needs.  I joined the Python Doc-SIG_
+(Documentation Special Interest Group) mailing list and found an
+ongoing discussion of the shortcomings of the StructuredText
+"standard".  This discussion has been going on since the inception of
+the mailing list in 1996, and possibly predates it.
+
+I decided to modify the original module with my own extensions and
+some suggested by the Doc-SIG members.  I soon realized that the
+module was not written with extension in mind, so I embarked upon a
+general reworking, including adapting it to the "re" regular
+expression module (the original inspiration for the name of this
+project).  Soon after I completed the modifications, I discovered that
+StructuredText.py was up to version 1.23 in the ZOPE distribution.
+Implementing the new syntax extensions from version 1.23 proved to be
+an exercise in frustration, as the complexity of the module had become
+overwhelming.
+
+In 2000, development on StructuredTextNG ("Next Generation") began at
+`Zope Corporation`_ (then Digital Creations).  It seems to have many
+improvements, but still suffers from many of the problems of classic
+StructuredText.
+
+I decided that a complete rewrite was in order, and even started a
+`reStructuredText SourceForge project`_ (now inactive).  My
+motivations (the "itches" I aim to "scratch") are as follows:
+
+- I need a standard format for inline documentation of the programs I
+  write.  This inline documentation has to be convertible to other
+  useful formats, such as HTML.  I believe many others have the same
+  need.
+
+- I believe in the Setext/StructuredText idea and want to help
+  formalize the standard.  However, I feel the current specifications
+  and implementations have flaws that desperately need fixing.
+
+- reStructuredText could form part of the foundation for a
+  documentation extraction and processing system, greatly benefitting
+  Python.  But it is only a part, not the whole.  reStructuredText is
+  a markup language specification and a reference parser
+  implementation, but it does not aspire to be the entire system.  I
+  don't want reStructuredText or a hypothetical Python documentation
+  processor to die stillborn because of over-ambition.
+
+- Most of all, I want to help ease the documentation chore, the bane
+  of many a programmer.
+
+Unfortunately I was sidetracked and stopped working on this project.
+In November 2000 I made the time to enumerate the problems of
+StructuredText and possible solutions, and complete the first draft of
+a specification.  This first draft was posted to the Doc-SIG in three
+parts:
+
+- `A Plan for Structured Text`__
+- `Problems With StructuredText`__
+- `reStructuredText: Revised Structured Text Specification`__
+
+__ https://mail.python.org/pipermail/doc-sig/2000-November/001239.html
+__ https://mail.python.org/pipermail/doc-sig/2000-November/001240.html
+__ https://mail.python.org/pipermail/doc-sig/2000-November/001241.html
+
+In March 2001 a flurry of activity on the Doc-SIG spurred me to
+further revise and refine my specification, the result of which you
+are now reading.  An offshoot of the reStructuredText project has been
+the realization that a single markup scheme, no matter how well
+thought out, may not be enough.  In order to tame the endless debates
+on Doc-SIG, a flexible `Docstring Processing System framework`_ needed
+to be constructed.  This framework has become the more important of
+the two projects; reStructuredText_ has found its place as one
+possible choice for a single component of the larger framework.
+
+The project web site and the first project release were rolled out in
+June 2001, including posting the second draft of the spec [#spec-2]_
+and the first draft of PEPs 256, 257, and 258 [#peps-1]_ to the
+Doc-SIG.  These documents and the project implementation proceeded to
+evolve at a rapid pace.  Implementation history details can be found
+in the `project history file`_.
+
+In November 2001, the reStructuredText parser was nearing completion.
+Development of the parser continued with the addition of small
+convenience features, improvements to the syntax, the filling in of
+gaps, and bug fixes.  After a long holiday break, in early 2002 most
+development moved over to the other Docutils components, the
+"Readers", "Writers", and "Transforms".  A "standalone" reader
+(processes standalone text file documents) was completed in February,
+and a basic HTML writer (producing HTML 4.01, using CSS-1) was
+completed in early March.
+
+`PEP 287`_, "reStructuredText Standard Docstring Format", was created
+to formally propose reStructuredText as a standard format for Python
+docstrings, PEPs, and other files.  It was first posted to
+comp.lang.python_ and the Python-dev_ mailing list on 2002-04-02.
+
+Version 0.4 of the reStructuredText__ and `Docstring Processing
+System`_ projects were released in April 2002.  The two projects were
+immediately merged, renamed to "Docutils_", and a 0.1 release soon
+followed.
+
+.. __: `reStructuredText SourceForge project`_
+
+.. [#spec-2] The second draft of the spec:
+
+   - `An Introduction to reStructuredText`__
+   - `Problems With StructuredText`__
+   - `reStructuredText Markup Specification`__
+   - `Python Extensions to the reStructuredText Markup
+     Specification`__
+
+   __ https://mail.python.org/pipermail/doc-sig/2001-June/001858.html
+   __ https://mail.python.org/pipermail/doc-sig/2001-June/001859.html
+   __ https://mail.python.org/pipermail/doc-sig/2001-June/001860.html
+   __ https://mail.python.org/pipermail/doc-sig/2001-June/001861.html
+
+.. [#peps-1] First drafts of the PEPs:
+
+   - `PEP 256: Docstring Processing System Framework`__
+   - `PEP 258: DPS Generic Implementation Details`__
+   - `PEP 257: Docstring Conventions`__
+
+   Current working versions of the PEPs can be found in
+   https://docutils.sourceforge.io/docs/peps/, and official versions
+   can be found in the `master PEP repository`_.
+
+   __ https://mail.python.org/pipermail/doc-sig/2001-June/001855.html
+   __ https://mail.python.org/pipermail/doc-sig/2001-June/001856.html
+   __ https://mail.python.org/pipermail/doc-sig/2001-June/001857.html
+
+
+.. _reStructuredText: https://docutils.sourceforge.io/rst.html
+.. _StructuredText: https://zopestructuredtext.readthedocs.org/
+.. _Setext: https://docutils.sourceforge.io/mirror/setext.html
+.. _Docutils: https://docutils.sourceforge.io/
+.. _Doc-SIG: https://www.python.org/sigs/doc-sig/
+.. _Zope Corporation: http://www.zope.com
+.. _ZOPE: https://www.zope.dev
+.. _reStructuredText SourceForge project:
+   http://structuredtext.sourceforge.net/
+.. _pythondoc: http://starship.python.net/crew/danilo/pythondoc/
+.. _project history file: ../../../HISTORY.html
+.. _PEP 287: ../../peps/pep-0287.html
+.. _Docstring Processing System framework: ../../peps/pep-0256.html
+.. _comp.lang.python: news:comp.lang.python
+.. _Python-dev: https://mail.python.org/pipermail/python-dev/
+.. _Docstring Processing System: http://docstring.sourceforge.net/
+.. _master PEP repository: https://peps.python.org/

Modified: trunk/docutils/docutils/utils/_roman_numerals.py
===================================================================
--- trunk/docutils/docutils/utils/_roman_numerals.py	2024-10-04 12:21:52 UTC (rev 9945)
+++ trunk/docutils/docutils/utils/_roman_numerals.py	2024-10-13 00:24:58 UTC (rev 9946)
@@ -1,247 +1,247 @@
-# $Id$
-# Author: Adam Turner.
-# Copyright: This module is placed in the public domain
-#            or under the `Zero Clause BSD licence`_,
-#            whichever is more permissive.
-#
-# .. _Zero Clause BSD licence: https://opensource.org/license/0BSD
-
-"""Conversion between integers and roman numerals."""
-
-from __future__ import annotations
-
-import sys
-from typing import TYPE_CHECKING, final
-
-if TYPE_CHECKING:
-    from typing import Final, Self
-
-__all__ = (
-    'MIN',
-    'MAX',
-    'RomanNumeral',
-    'OutOfRangeError',
-    'InvalidRomanNumeralError',
-)
-
-MIN: Final = 1
-"""The value of the smallest well-formed roman numeral."""
-
-# Note that 4,999 (MMMMCMXCIX) breaks one of the rules of Roman numerals,
-# that the same character may not appear more than thrice consecutively,
-# meaning the largest 'well-formed' Roman numeral is 3,999 (MMMCMXCIX).
-# We use 4,999 for backwards compatibility reasons.
-MAX: Final = 4_999
-"""The value of the largest well-formed roman numeral."""
-
-
-class OutOfRangeError(TypeError):
-    """Number out of range (must be between 1 and 4,999)."""
-
-
-class InvalidRomanNumeralError(ValueError):
-    """Not a valid Roman numeral."""
-
-    def __init__(self, value, *args):
-        msg = f'Invalid Roman numeral: {value}'
-        super().__init__(msg, *args)
-
-
-@final
-class RomanNumeral:
-    """A Roman numeral.
-
-    Only values between 1 and 4,999 are valid.
-    Stores the value internally as an ``int``.
-
-    >>> answer = RomanNumeral(42)
-    >>> print(answer.to_uppercase())
-    XLII
-    """
-    __slots__ = ('_value',)
-    _value: int
-
-    def __init__(self, value: int, /) -> None:
-        if not isinstance(value, int):
-            msg = 'RomanNumeral: an integer is required, not %r'
-            raise TypeError(msg % type(value).__qualname__)
-        if value < MIN or value > MAX:
-            msg = 'Number out of range (must be between 1 and 4,999). Got %s.'
-            raise OutOfRangeError(msg % value)
-        super().__setattr__('_value', value)
-
-    def __int__(self) -> int:
-        return self._value
-
-    def __str__(self) -> str:
-        return self.to_uppercase()
-
-    def __repr__(self):
-        return f'{self.__class__.__name__}({self._value!r})'
-
-    def __eq__(self, other):
-        if isinstance(other, RomanNumeral):
-            return self._value == other._value
-        return NotImplemented
-
-    def __lt__(self, other):
-        if isinstance(other, RomanNumeral):
-            return self._value < other._value
-        return NotImplemented
-
-    def __hash__(self):
-        return hash(self._value)
-
-    def __setattr__(self, key, value):
-        if key == '_value':
-            raise AttributeError('Cannot set the value attribute.')
-        super().__setattr__(key, value)
-
-    def to_uppercase(self) -> str:
-        """Converts a ``RomanNumeral`` to an uppercase string.
-
-        >>> answer = RomanNumeral(42)
-        >>> assert answer.to_uppercase() == 'XLII'
-        """
-        out = []
-        n = int(self)
-        for value, name, _ in _ROMAN_NUMERAL_PREFIXES:
-            while n >= value:
-                n -= value
-                out.append(name)
-        return ''.join(out)
-
-    def to_lowercase(self) -> str:
-        """Converts a ``RomanNumeral`` to a lowercase string.
-
-        >>> answer = RomanNumeral(42)
-        >>> assert answer.to_lowercase() == 'xlii'
-        """
-        out = []
-        n = int(self)
-        for value, _, name in _ROMAN_NUMERAL_PREFIXES:
-            while n >= value:
-                n -= value
-                out.append(name)
-        return ''.join(out)
-
-    @classmethod
-    def from_string(self, string: str, /) -> Self:
-        """Creates a ``RomanNumeral`` from a well-formed string representation.
-
-        Returns ``RomanNumeral`` or raises ``InvalidRomanNumeralError``.
-
-        >>> answer = RomanNumeral.from_string('XLII')
-        >>> assert int(answer) == 42
-        """
-        # Not an empty string.
-        if not string or not isinstance(string, str):
-            raise InvalidRomanNumeralError(string)
-
-        # ASCII-only uppercase string.
-        if string.isascii() and string.isupper():
-            chars = string.encode('ascii')
-        elif string.isascii() and string.islower():
-            chars = string.upper().encode('ascii')
-        else:
-            # Either Non-ASCII or mixed-case ASCII.
-            raise InvalidRomanNumeralError(string)
-
-        # ASCII-only uppercase string only containing I, V, X, L, C, D, M.
-        if not frozenset(b'IVXLCDM').issuperset(chars):
-            raise InvalidRomanNumeralError(string)
-
-        result: int = 0
-        idx: int = 0
-
-        # Thousands: between 0 and 4 "M" characters at the start
-        for _ in range(4):
-            if chars[idx:idx + 1] == b'M':
-                result += 1000
-                idx += 1
-            else:
-                break
-        if len(chars) == idx:
-            return RomanNumeral(result)
-
-        # Hundreds: 900 ("CM"), 400 ("CD"), 0-300 (0 to 3 "C" chars),
-        # or 500-800 ("D", followed by 0 to 3 "C" chars)
-        if chars[idx:idx + 2] == b'CM':
-            result += 900
-            idx += 2
-        elif chars[idx:idx + 2] == b'CD':
-            result += 400
-            idx += 2
-        else:
-            if chars[idx:idx + 1] == b'D':
-                result += 500
-                idx += 1
-            for _ in range(3):
-                if chars[idx:idx + 1] == b'C':
-                    result += 100
-                    idx += 1
-                else:
-                    break
-        if len(chars) == idx:
-            return RomanNumeral(result)
-
-        # Tens: 90 ("XC"), 40 ("XL"), 0-30 (0 to 3 "X" chars),
-        # or 50-80 ("L", followed by 0 to 3 "X" chars)
-        if chars[idx:idx + 2] == b'XC':
-            result += 90
-            idx += 2
-        elif chars[idx:idx + 2] == b'XL':
-            result += 40
-            idx += 2
-        else:
-            if chars[idx:idx + 1] == b'L':
-                result += 50
-                idx += 1
-            for _ in range(3):
-                if chars[idx:idx + 1] == b'X':
-                    result += 10
-                    idx += 1
-                else:
-                    break
-        if len(chars) == idx:
-            return RomanNumeral(result)
-
-        # Ones: 9 ("IX"), 4 ("IV"), 0-3 (0 to 3 "I" chars),
-        # or 5-8 ("V", followed by 0 to 3 "I" chars)
-        if chars[idx:idx + 2] == b'IX':
-            result += 9
-            idx += 2
-        elif chars[idx:idx + 2] == b'IV':
-            result += 4
-            idx += 2
-        else:
-            if chars[idx:idx + 1] == b'V':
-                result += 5
-                idx += 1
-            for _ in range(3):
-                if chars[idx:idx + 1] == b'I':
-                    result += 1
-                    idx += 1
-                else:
-                    break
-        if len(chars) == idx:
-            return RomanNumeral(result)
-        raise InvalidRomanNumeralError(string)
-
-
-_ROMAN_NUMERAL_PREFIXES: Final = [
-    (1000, sys.intern('M'), sys.intern('m')),
-    (900, sys.intern('CM'), sys.intern('cm')),
-    (500, sys.intern('D'), sys.intern('d')),
-    (400, sys.intern('CD'), sys.intern('cd')),
-    (100, sys.intern('C'), sys.intern('c')),
-    (90, sys.intern('XC'), sys.intern('xc')),
-    (50, sys.intern('L'), sys.intern('l')),
-    (40, sys.intern('XL'), sys.intern('xl')),
-    (10, sys.intern('X'), sys.intern('x')),
-    (9, sys.intern('IX'), sys.intern('ix')),
-    (5, sys.intern('V'), sys.intern('v')),
-    (4, sys.intern('IV'), sys.intern('iv')),
-    (1, sys.intern('I'), sys.intern('i')),
-]
-"""Numeral value, uppercase character, and lowercase character."""
+# $Id$
+# Author: Adam Turner.
+# Copyright: This module is placed in the public domain
+#            or under the `Zero Clause BSD licence`_,
+#            whichever is more permissive.
+#
+# .. _Zero Clause BSD licence: https://opensource.org/license/0BSD
+
+"""Conversion between integers and roman numerals."""
+
+from __future__ import annotations
+
+import sys
+from typing import TYPE_CHECKING, final
+
+if TYPE_CHECKING:
+    from typing import Final, Self
+
+__all__ = (
+    'MIN',
+    'MAX',
+    'RomanNumeral',
+    'OutOfRangeError',
+    'InvalidRomanNumeralError',
+)
+
+MIN: Final = 1
+"""The value of the smallest well-formed roman numeral."""
+
+# Note that 4,999 (MMMMCMXCIX) breaks one of the rules of Roman numerals,
+# that the same character may not appear more than thrice consecutively,
+# meaning the largest 'well-formed' Roman numeral is 3,999 (MMMCMXCIX).
+# We use 4,999 for backwards compatibility reasons.
+MAX: Final = 4_999
+"""The value of the largest well-formed roman numeral."""
+
+
+class OutOfRangeError(TypeError):
+    """Number out of range (must be between 1 and 4,999)."""
+
+
+class InvalidRomanNumeralError(ValueError):
+    """Not a valid Roman numeral."""
+
+    def __init__(self, value, *args):
+        msg = f'Invalid Roman numeral: {value}'
+        super().__init__(msg, *args)
+
+
+@final
+class RomanNumeral:
+    """A Roman numeral.
+
+    Only values between 1 and 4,999 are valid.
+    Stores the value internally as an ``int``.
+
+    >>> answer = RomanNumeral(42)
+    >>> print(answer.to_uppercase())
+    XLII
+    """
+    __slots__ = ('_value',)
+    _value: int
+
+    def __init__(self, value: int, /) -> None:
+        if not isinstance(value, int):
+            msg = 'RomanNumeral: an integer is required, not %r'
+            raise TypeError(msg % type(value).__qualname__)
+        if value < MIN or value > MAX:
+            msg = 'Number out of range (must be between 1 and 4,999). Got %s.'
+            raise OutOfRangeError(msg % value)
+        super().__setattr__('_value', value)
+
+    def __int__(self) -> int:
+        return self._value
+
+    def __str__(self) -> str:
+        return self.to_uppercase()
+
+    def __repr__(self):
+        return f'{self.__class__.__name__}({self._value!r})'
+
+    def __eq__(self, other):
+        if isinstance(other, RomanNumeral):
+            return self._value == other._value
+        return NotImplemented
+
+    def __lt__(self, other):
+        if isinstance(other, RomanNumeral):
+            return self._value < other._value
+        return NotImplemented
+
+    def __hash__(self):
+        return hash(self._value)
+
+    def __setattr__(self, key, value):
+        if key == '_value':
+            raise AttributeError('Cannot set the value attribute.')
+        super().__setattr__(key, value)
+
+    def to_uppercase(self) -> str:
+        """Converts a ``RomanNumeral`` to an uppercase string.
+
+        >>> answer = RomanNumeral(42)
+        >>> assert answer.to_uppercase() == 'XLII'
+        """
+        out = []
+        n = int(self)
+        for value, name, _ in _ROMAN_NUMERAL_PREFIXES:
+            while n >= value:
+                n -= value
+                out.append(name)
+        return ''.join(out)
+
+    def to_lowercase(self) -> str:
+        """Converts a ``RomanNumeral`` to a lowercase string.
+
+        >>> answer = RomanNumeral(42)
+        >>> assert answer.to_lowercase() == 'xlii'
+        """
+        out = []
+        n = int(self)
+        for value, _, name in _ROMAN_NUMERAL_PREFIXES:
+            while n >= value:
+                n -= value
+                out.append(name)
+        return ''.join(out)
+
+    @classmethod
+    def from_string(self, string: str, /) -> Self:
+        """Creates a ``RomanNumeral`` from a well-formed string representation.
+
+        Returns ``RomanNumeral`` or raises ``InvalidRomanNumeralError``.
+
+        >>> answer = RomanNumeral.from_string('XLII')
+        >>> assert int(answer) == 42
+        """
+        # Not an empty string.
+        if not string or not isinstance(string, str):
+            raise InvalidRomanNumeralError(string)
+
+        # ASCII-only uppercase string.
+        if string.isascii() and string.isupper():
+            chars = string.encode('ascii')
+        elif string.isascii() and string.islower():
+            chars = string.upper().encode('ascii')
+        else:
+            # Either Non-ASCII or mixed-case ASCII.
+            raise InvalidRomanNumeralError(string)
+
+        # ASCII-only uppercase string only containing I, V, X, L, C, D, M.
+        if not frozenset(b'IVXLCDM').issuperset(chars):
+            raise InvalidRomanNumeralError(string)
+
+        result: int = 0
+        idx: int = 0
+
+        # Thousands: between 0 and 4 "M" characters at the start
+        for _ in range(4):
+            if chars[idx:idx + 1] == b'M':
+                result += 1000
+                idx += 1
+            else:
+                break
+        if len(chars) == idx:
+            return RomanNumeral(result)
+
+        # Hundreds: 900 ("CM"), 400 ("CD"), 0-300 (0 to 3 "C" chars),
+        # or 500-800 ("D", followed by 0 to 3 "C" chars)
+        if chars[idx:idx + 2] == b'CM':
+            result += 900
+            idx += 2
+        elif chars[idx:idx + 2] == b'CD':
+            result += 400
+            idx += 2
+        else:
+            if chars[idx:idx + 1] == b'D':
+                result += 500
+                idx += 1
+            for _ in range(3):
+                if chars[idx:idx + 1] == b'C':
+                    result += 100
+                    idx += 1
+                else:
+                    break
+        if len(chars) == idx:
+            return RomanNumeral(result)
+
+        # Tens: 90 ("XC"), 40 ("XL"), 0-30 (0 to 3 "X" chars),
+        # or 50-80 ("L", followed by 0 to 3 "X" chars)
+        if chars[idx:idx + 2] == b'XC':
+            result += 90
+            idx += 2
+        elif chars[idx:idx + 2] == b'XL':
+            result += 40
+            idx += 2
+        else:
+            if chars[idx:idx + 1] == b'L':
+                result += 50
+                idx += 1
+            for _ in range(3):
+                if chars[idx:idx + 1] == b'X':
+                    result += 10
+                    idx += 1
+                else:
+                    break
+        if len(chars) == idx:
+            return RomanNumeral(result)
+
+        # Ones: 9 ("IX"), 4 ("IV"), 0-3 (0 to 3 "I" chars),
+        # or 5-8 ("V", followed by 0 to 3 "I" chars)
+        if chars[idx:idx + 2] == b'IX':
+            result += 9
+            idx += 2
+        elif chars[idx:idx + 2] == b'IV':
+            result += 4
+            idx += 2
+        else:
+            if chars[idx:idx + 1] == b'V':
+                result += 5
+                idx += 1
+            for _ in range(3):
+                if chars[idx:idx + 1] == b'I':
+                    result += 1
+                    idx += 1
+                else:
+                    break
+        if len(chars) == idx:
+            return RomanNumeral(result)
+        raise InvalidRomanNumeralError(string)
+
+
+_ROMAN_NUMERAL_PREFIXES: Final = [
+    (1000, sys.intern('M'), sys.intern('m')),
+    (900, sys.intern('CM'), sys.intern('cm')),
+    (500, sys.intern('D'), sys.intern('d')),
+    (400, sys.intern('CD'), sys.intern('cd')),
+    (100, sys.intern('C'), sys.intern('c')),
+    (90, sys.intern('XC'), sys.intern('xc')),
+    (50, sys.intern('L'), sys.intern('l')),
+    (40, sys.intern('XL'), sys.intern('xl')),
+    (10, sys.intern('X'), sys.intern('x')),
+    (9, sys.intern('IX'), sys.intern('ix')),
+    (5, sys.intern('V'), sys.intern('v')),
+    (4, sys.intern('IV'), sys.intern('iv')),
+    (1, sys.intern('I'), sys.intern('i')),
+]
+"""Numeral value, uppercase character, and lowercase character."""

Modified: trunk/docutils/licenses/BSD-0-Clause.rst
===================================================================
--- trunk/docutils/licenses/BSD-0-Clause.rst	2024-10-04 12:21:52 UTC (rev 9945)
+++ trunk/docutils/licenses/BSD-0-Clause.rst	2024-10-13 00:24:58 UTC (rev 9946)
@@ -1,13 +1,13 @@
-Zero-Clause BSD
-===============
-
-Permission to use, copy, modify, and/or distribute this software for
-any purpose with or without fee is hereby granted.
-
-THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
-WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
-OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
-FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
-DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
-AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
-OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+Zero-Clause BSD
+===============
+
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

Modified: trunk/docutils/test/test_utils/test__roman_numerals.py
===================================================================
--- trunk/docutils/test/test_utils/test__roman_numerals.py	2024-10-04 12:21:52 UTC (rev 9945)
+++ trunk/docutils/test/test_utils/test__roman_numerals.py	2024-10-13 00:24:58 UTC (rev 9946)
@@ -1,210 +1,210 @@
-#! /usr/bin/env python3
-
-# $Id$
-# Author: Adam Turner.
-# Copyright: This module is placed in the public domain
-#            or under the `Zero Clause BSD licence`_,
-#            whichever is more permissive.
-#
-# .. _Zero Clause BSD licence: https://opensource.org/license/0BSD
-
-"""
-Tests for `docutils.utils._roman_numerals`.
-"""
-
-from pathlib import Path
-import sys
-import unittest
-
-if __name__ == '__main__':
-    # prepend the "docutils root" to the Python library path
-    # so we import the local `docutils` package.
-    sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
-
-from docutils.utils._roman_numerals import (
-    MIN,
-    MAX,
-    RomanNumeral,
-    OutOfRangeError,
-    InvalidRomanNumeralError,
-)
-
-
-class NewRomanNumeralTestCase(unittest.TestCase):
-    def test_zero(self) -> None:
-        with self.assertRaises(OutOfRangeError) as ctx:
-            RomanNumeral(0)
-        msg = str(ctx.exception)
-        self.assertEqual(
-            msg,
-            'Number out of range (must be between 1 and 4,999). Got 0.',
-        )
-
-    def test_one(self) -> None:
-        self.assertEqual(int(RomanNumeral(1)), 1)
-
-    def test_MIN(self) -> None:
-        self.assertEqual(int(RomanNumeral(MIN)), MIN)
-
-    def test_forty_two(self) -> None:
-        self.assertEqual(int(RomanNumeral(42)), 42)
-
-    def test_four_thousand_nine_hundred_and_ninety_nine(self) -> None:
-        self.assertEqual(int(RomanNumeral(4_999)), 4_999)
-
-    def test_MAX(self) -> None:
-        self.assertEqual(int(RomanNumeral(MAX)), MAX)
-
-    def test_five_thousand(self) -> None:
-        with self.assertRaises(OutOfRangeError) as ctx:
-            RomanNumeral(5_000)
-        msg = str(ctx.exception)
-        self.assertEqual(
-            msg,
-            'Number out of range (must be between 1 and 4,999). Got 5000.',
-        )
-
-    def test_minus_one(self) -> None:
-        with self.assertRaises(OutOfRangeError) as ctx:
-            RomanNumeral(-1)
-        msg = str(ctx.exception)
-        self.assertEqual(
-            msg,
-            'Number out of range (must be between 1 and 4,999). Got -1.',
-        )
-
-    def test_float(self) -> None:
-        with self.assertRaises(TypeError) as ctx:
-            RomanNumeral(4.2)
-        msg = str(ctx.exception)
-        self.assertEqual(
-            msg,
-            "RomanNumeral: an integer is required, not 'float'",
-        )
-
-
-class ToStringTestCase(unittest.TestCase):
-    def test_str(self):
-        test_numerals = [
-            'I', 'II', 'III', 'IV', 'V',
-            'VI', 'VII', 'VIII', 'IX', 'X',
-            'XI', 'XII', 'XIII', 'XIV', 'XV',
-            'XVI', 'XVII', 'XVIII', 'XIX', 'XX',
-            'XXI', 'XXII', 'XXIII', 'XXIV',
-        ]
-        for n, roman_str in enumerate(test_numerals, start=1):
-            with self.subTest(id=n, roman_str=roman_str):
-                num = RomanNumeral(n)
-                self.assertEqual(f'{num}', roman_str)
-
-    def test_uppercase(self):
-        test_numerals = [
-            'I', 'II', 'III', 'IV', 'V',
-            'VI', 'VII', 'VIII', 'IX', 'X',
-            'XI', 'XII', 'XIII', 'XIV', 'XV',
-            'XVI', 'XVII', 'XVIII', 'XIX', 'XX',
-            'XXI', 'XXII', 'XXIII', 'XXIV',
-        ]
-        for n, roman_str in enumerate(test_numerals, start=1):
-            with self.subTest(id=n, roman_str=roman_str):
-                num = RomanNumeral(n)
-                self.assertEqual(num.to_uppercase(), roman_str)
-
-    def test_lowercase(self):
-        test_numerals = [
-            'i', 'ii', 'iii', 'iv', 'v',
-            'vi', 'vii', 'viii', 'ix', 'x',
-            'xi', 'xii', 'xiii', 'xiv', 'xv',
-            'xvi', 'xvii', 'xviii', 'xix', 'xx',
-            'xxi', 'xxii', 'xxiii', 'xxiv',
-        ]
-        for n, roman_str in enumerate(test_numerals, start=1):
-            with self.subTest(id=n, roman_str=roman_str):
-                num = RomanNumeral(n)
-                self.assertEqual(num.to_lowercase(), roman_str)
-
-    def test_minitrue(self):
-        # IGNORANCE IS STRENGTH
-        num = RomanNumeral(1984)
-        self.assertEqual(f'{num}', 'MCMLXXXIV')
-        self.assertEqual(num.to_uppercase(), 'MCMLXXXIV')
-        self.assertEqual(num.to_lowercase(), 'mcmlxxxiv')
-
-
-class FromStringTestCase(unittest.TestCase):
-    def test_uppercase(self):
-        test_numerals = [
-            'I', 'II', 'III', 'IV', 'V',
-            'VI', 'VII', 'VIII', 'IX', 'X',
-            'XI', 'XII', 'XIII', 'XIV', 'XV',
-            'XVI', 'XVII', 'XVIII', 'XIX', 'XX',
-            'XXI', 'XXII', 'XXIII', 'XXIV',
-        ]
-        for n, roman_str in enumerate(test_numerals, start=1):
-            with self.subTest(id=n, roman_str=roman_str):
-                expected = RomanNumeral(n)
-                parsed = RomanNumeral.from_string(roman_str)
-                self.assertEqual(expected, parsed)
-
-    def test_lowercase(self):
-        test_numerals = [
-            'i', 'ii', 'iii', 'iv', 'v',
-            'vi', 'vii', 'viii', 'ix', 'x',
-            'xi', 'xii', 'xiii', 'xiv', 'xv',
-            'xvi', 'xvii', 'xviii', 'xix', 'xx',
-            'xxi', 'xxii', 'xxiii', 'xxiv',
-        ]
-        for n, roman_str in enumerate(test_numerals, start=1):
-            with self.subTest(id=n, roman_str=roman_str):
-                expected = RomanNumeral(n)
-                parsed = RomanNumeral.from_string(roman_str)
-                self.assertEqual(expected, parsed)
-
-    def test_special(self):
-        parsed = RomanNumeral.from_string('MDCCCXXIII')
-        self.assertEqual(RomanNumeral(1823), parsed)
-
-        parsed = RomanNumeral.from_string('mdcccxxiii')
-        self.assertEqual(RomanNumeral(1823), parsed)
-
-        parsed = RomanNumeral.from_string('MCMLXXXIV')
-        self.assertEqual(RomanNumeral(1984), parsed)
-
-        parsed = RomanNumeral.from_string('mcmlxxxiv')
-        self.assertEqual(RomanNumeral(1984), parsed)
-
-        parsed = RomanNumeral.from_string('MM')
-        self.assertEqual(RomanNumeral(2000), parsed)
-
-        parsed = RomanNumeral.from_string('mm')
-        self.assertEqual(RomanNumeral(2000), parsed)
-
-        parsed = RomanNumeral.from_string('MMMMCMXCIX')
-        self.assertEqual(RomanNumeral(4_999), parsed)
-
-        parsed = RomanNumeral.from_string('mmmmcmxcix')
-        self.assertEqual(RomanNumeral(4_999), parsed)
-
-    def test_invalid(self):
-        with self.assertRaises(InvalidRomanNumeralError) as ctx:
-            RomanNumeral.from_string('Not a Roman numeral!')
-        msg = str(ctx.exception)
-        self.assertEqual(msg, 'Invalid Roman numeral: Not a Roman numeral!')
-
-    def test_mixed_case(self):
-        with self.assertRaises(InvalidRomanNumeralError) as ctx:
-            RomanNumeral.from_string('McMlXxXiV')
-        msg = str(ctx.exception)
-        self.assertEqual(msg, 'Invalid Roman numeral: McMlXxXiV')
-
-
-class RoundTripTestCase(unittest.TestCase):
-    def test_round_trip(self):
-        for n in range(MIN, MAX + 1, 19):
-            num = RomanNumeral(n)
-            parsed = RomanNumeral.from_string(str(num))
-            self.assertEqual(num, parsed)
-
-
-if __name__ == '__main__':
-    unittest.main()
+#! /usr/bin/env python3
+
+# $Id$
+# Author: Adam Turner.
+# Copyright: This module is placed in the public domain
+#            or under the `Zero Clause BSD licence`_,
+#            whichever is more permissive.
+#
+# .. _Zero Clause BSD licence: https://opensource.org/license/0BSD
+
+"""
+Tests for `docutils.utils._roman_numerals`.
+"""
+
+from pathlib import Path
+import sys
+import unittest
+
+if __name__ == '__main__':
+    # prepend the "docutils root" to the Python library path
+    # so we import the local `docutils` package.
+    sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
+
+from docutils.utils._roman_numerals import (
+    MIN,
+    MAX,
+    RomanNumeral,
+    OutOfRangeError,
+    InvalidRomanNumeralError,
+)
+
+
+class NewRomanNumeralTestCase(unittest.TestCase):
+    def test_zero(self) -> None:
+        with self.assertRaises(OutOfRangeError) as ctx:
+            RomanNumeral(0)
+        msg = str(ctx.exception)
+        self.assertEqual(
+            msg,
+            'Number out of range (must be between 1 and 4,999). Got 0.',
+        )
+
+    def test_one(self) -> None:
+        self.assertEqual(int(RomanNumeral(1)), 1)
+
+    def test_MIN(self) -> None:
+        self.assertEqual(int(RomanNumeral(MIN)), MIN)
+
+    def test_forty_two(self) -> None:
+        self.assertEqual(int(RomanNumeral(42)), 42)
+
+    def test_four_thousand_nine_hundred_and_ninety_nine(self) -> None:
+        self.assertEqual(int(RomanNumeral(4_999)), 4_999)
+
+    def test_MAX(self) -> None:
+        self.assertEqual(int(RomanNumeral(MAX)), MAX)
+
+    def test_five_thousand(self) -> None:
+        with self.assertRaises(OutOfRangeError) as ctx:
+            RomanNumeral(5_000)
+        msg = str(ctx.exception)
+        self.assertEqual(
+            msg,
+            'Number out of range (must be between 1 and 4,999). Got 5000.',
+        )
+
+    def test_minus_one(self) -> None:
+        with self.assertRaises(OutOfRangeError) as ctx:
+            RomanNumeral(-1)
+        msg = str(ctx.exception)
+        self.assertEqual(
+            msg,
+            'Number out of range (must be between 1 and 4,999). Got -1.',
+        )
+
+    def test_float(self) -> None:
+        with self.assertRaises(TypeError) as ctx:
+            RomanNumeral(4.2)
+        msg = str(ctx.exception)
+        self.assertEqual(
+            msg,
+            "RomanNumeral: an integer is required, not 'float'",
+        )
+
+
+class ToStringTestCase(unittest.TestCase):
+    def test_str(self):
+        test_numerals = [
+            'I', 'II', 'III', 'IV', 'V',
+            'VI', 'VII', 'VIII', 'IX', 'X',
+            'XI', 'XII', 'XIII', 'XIV', 'XV',
+            'XVI', 'XVII', 'XVIII', 'XIX', 'XX',
+            'XXI', 'XXII', 'XXIII', 'XXIV',
+        ]
+        for n, roman_str in enumerate(test_numerals, start=1):
+            with self.subTest(id=n, roman_str=roman_str):
+                num = RomanNumeral(n)
+                self.assertEqual(f'{num}', roman_str)
+
+    def test_uppercase(self):
+        test_numerals = [
+            'I', 'II', 'III', 'IV', 'V',
+            'VI', 'VII', 'VIII', 'IX', 'X',
+            'XI', 'XII', 'XIII', 'XIV', 'XV',
+            'XVI', 'XVII', 'XVIII', 'XIX', 'XX',
+            'XXI', 'XXII', 'XXIII', 'XXIV',
+        ]
+        for n, roman_str in enumerate(test_numerals, start=1):
+            with self.subTest(id=n, roman_str=roman_str):
+                num = RomanNumeral(n)
+                self.assertEqual(num.to_uppercase(), roman_str)
+
+    def test_lowercase(self):
+        test_numerals = [
+            'i', 'ii', 'iii', 'iv', 'v',
+            'vi', 'vii', 'viii', 'ix', 'x',
+            'xi', 'xii', 'xiii', 'xiv', 'xv',
+            'xvi', 'xvii', 'xviii', 'xix', 'xx',
+            'xxi', 'xxii', 'xxiii', 'xxiv',
+        ]
+        for n, roman_str in enumerate(test_numerals, start=1):
+            with self.subTest(id=n, roman_str=roman_str):
+                num = RomanNumeral(n)
+                self.assertEqual(num.to_lowercase(), roman_str)
+
+    def test_minitrue(self):
+        # IGNORANCE IS STRENGTH
+        num = RomanNumeral(1984)
+        self.assertEqual(f'{num}', 'MCMLXXXIV')
+        self.assertEqual(num.to_uppercase(), 'MCMLXXXIV')
+        self.assertEqual(num.to_lowercase(), 'mcmlxxxiv')
+
+
+class FromStringTestCase(unittest.TestCase):
+    def test_uppercase(self):
+        test_numerals = [
+            'I', 'II', 'III', 'IV', 'V',
+            'VI', 'VII', 'VIII', 'IX', 'X',
+            'XI', 'XII', 'XIII', 'XIV', 'XV',
+            'XVI', 'XVII', 'XVIII', 'XIX', 'XX',
+            'XXI', 'XXII', 'XXIII', 'XXIV',
+        ]
+        for n, roman_str in enumerate(test_numerals, start=1):
+            with self.subTest(id=n, roman_str=roman_str):
+                expected = RomanNumeral(n)
+                parsed = RomanNumeral.from_string(roman_str)
+                self.assertEqual(expected, parsed)
+
+    def test_lowercase(self):
+        test_numerals = [
+            'i', 'ii', 'iii', 'iv', 'v',
+            'vi', 'vii', 'viii', 'ix', 'x',
+            'xi', 'xii', 'xiii', 'xiv', 'xv',
+            'xvi', 'xvii', 'xviii', 'xix', 'xx',
+            'xxi', 'xxii', 'xxiii', 'xxiv',
+        ]
+        for n, roman_str in enumerate(test_numerals, start=1):
+            with self.subTest(id=n, roman_str=roman_str):
+                expected = RomanNumeral(n)
+                parsed = RomanNumeral.from_string(roman_str)
+                self.assertEqual(expected, parsed)
+
+    def test_special(self):
+        parsed = RomanNumeral.from_string('MDCCCXXIII')
+        self.assertEqual(RomanNumeral(1823), parsed)
+
+        parsed = RomanNumeral.from_string('mdcccxxiii')
+        self.assertEqual(RomanNumeral(1823), parsed)
+
+        parsed = RomanNumeral.from_string('MCMLXXXIV')
+        self.assertEqual(RomanNumeral(1984), parsed)
+
+        parsed = RomanNumeral.from_string('mcmlxxxiv')
+        self.assertEqual(RomanNumeral(1984), parsed)
+
+        parsed = RomanNumeral.from_string('MM')
+        self.assertEqual(RomanNumeral(2000), parsed)
+
+        parsed = RomanNumeral.from_string('mm')
+        self.assertEqual(RomanNumeral(2000), parsed)
+
+        parsed = RomanNumeral.from_string('MMMMCMXCIX')
+        self.assertEqual(RomanNumeral(4_999), parsed)
+
+        parsed = RomanNumeral.from_string('mmmmcmxcix')
+        self.assertEqual(RomanNumeral(4_999), parsed)
+
+    def test_invalid(self):
+        with self.assertRaises(InvalidRomanNumeralError) as ctx:
+            RomanNumeral.from_string('Not a Roman numeral!')
+        msg = str(ctx.exception)
+        self.assertEqual(msg, 'Invalid Roman numeral: Not a Roman numeral!')
+
+    def test_mixed_case(self):
+        with self.assertRaises(InvalidRomanNumeralError) as ctx:
+            RomanNumeral.from_string('McMlXxXiV')
+        msg = str(ctx.exception)
+        self.assertEqual(msg, 'Invalid Roman numeral: McMlXxXiV')
+
+
+class RoundTripTestCase(unittest.TestCase):
+    def test_round_trip(self):
+        for n in range(MIN, MAX + 1, 19):
+            num = RomanNumeral(n)
+            parsed = RomanNumeral.from_string(str(num))
+            self.assertEqual(num, parsed)
+
+
+if __name__ == '__main__':
+    unittest.main()

This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.



_______________________________________________
Docutils-checkins mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/docutils-checkins
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.