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