Skip to content

Commit

Permalink
Merge pull request #445 from collective/doctest
Browse files Browse the repository at this point in the history
test .py and .rst files with doctest
  • Loading branch information
niccokunzmann committed Nov 2, 2022
2 parents 7b1f376 + 0fbba53 commit 3e6777d
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Expand Up @@ -63,6 +63,7 @@ New features:

- icalendar utility outputs a 'Duration' row
- icalendar can take multiple ics files as an input
- source code in documentation is tested using doctest #445 [niccokunzmann]

Bug fixes:

Expand Down
2 changes: 1 addition & 1 deletion docs/install.rst
Expand Up @@ -112,7 +112,7 @@ Try it out:
Type "help", "copyright", "credits" or "license" for more information.
>>> import icalendar
>>> icalendar.__version__
'4.0.10.dev0'
'5.0.1'
Building the documentation locally
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down
5 changes: 4 additions & 1 deletion docs/maintenance.rst
Expand Up @@ -56,7 +56,10 @@ However, only people with ``PyPI environment access for GitHub Actions`` can app

1. Check that the ``CHANGES.rst`` is up to date with the `latest merged pull requests <https://github.com/collective/icalendar/pulls?q=is%3Apr+is%3Amerged>`__
and the version you want to release is correctly named.
2. Change the ``__version__`` variable in the ``src/icalendar/__init__.py`` file.
2. Change the ``__version__`` variable in

- the ``src/icalendar/__init__.py`` file and
- in the ``docs/usage.rst`` file (look for ``icalendar.__version__``)
3. Create a commit on the ``release-5.0.0`` branch (or equivalent) to release this version.

.. code-block:: bash
Expand Down
31 changes: 16 additions & 15 deletions docs/usage.rst
Expand Up @@ -79,8 +79,8 @@ you do it like this. The calendar is a component::
>>> cal['summary'] = 'Python meeting about calendaring'
>>> for k,v in cal.items():
... k,v
(u'DTSTART', '20050404T080000')
(u'SUMMARY', 'Python meeting about calendaring')
('DTSTART', '20050404T080000')
('SUMMARY', 'Python meeting about calendaring')

NOTE: the recommended way to add components to the calendar is to use
create the subcomponent and add it via Calendar.add! The example above adds a
Expand All @@ -90,7 +90,7 @@ string, but not a vText component.
You can generate a string for a file with the to_ical() method::

>>> cal.to_ical()
'BEGIN:VCALENDAR\r\nDTSTART:20050404T080000\r\nSUMMARY:Python meeting about calendaring\r\nEND:VCALENDAR\r\n'
b'BEGIN:VCALENDAR\r\nDTSTART:20050404T080000\r\nSUMMARY:Python meeting about calendaring\r\nEND:VCALENDAR\r\n'

The rendered view is easier to read::

Expand All @@ -102,13 +102,13 @@ The rendered view is easier to read::
So, let's define a function so we can easily display to_ical() output::

>>> def display(cal):
... return cal.to_ical().decode("utf-8").replace(b'\r\n', b'\n').strip()
... return cal.to_ical().decode("utf-8").replace('\r\n', '\n').strip()

You can set multiple properties like this::

>>> cal = Calendar()
>>> cal['attendee'] = ['MAILTO:maxm@mxm.dk','MAILTO:test@example.com']
>>> print display(cal)
>>> print(display(cal))
BEGIN:VCALENDAR
ATTENDEE:MAILTO:maxm@mxm.dk
ATTENDEE:MAILTO:test@example.com
Expand All @@ -122,7 +122,7 @@ added. Here is an example::
>>> cal = Calendar()
>>> cal.add('attendee', 'MAILTO:maxm@mxm.dk')
>>> cal.add('attendee', 'MAILTO:test@example.com')
>>> print display(cal)
>>> print(display(cal))
BEGIN:VCALENDAR
ATTENDEE:MAILTO:maxm@mxm.dk
ATTENDEE:MAILTO:test@example.com
Expand All @@ -148,7 +148,7 @@ component::
And then appending it to a "parent"::

>>> cal.add_component(event)
>>> print display(cal)
>>> print(display(cal))
BEGIN:VCALENDAR
ATTENDEE:MAILTO:maxm@mxm.dk
ATTENDEE:MAILTO:test@example.com
Expand All @@ -161,7 +161,7 @@ And then appending it to a "parent"::
Subcomponents are appended to the subcomponents property on the component::

>>> cal.subcomponents
[VEVENT({'DTSTART': '20050404T080000', 'UID': '42'})]
[VEVENT({'UID': '42', 'DTSTART': '20050404T080000'})]


Value types
Expand All @@ -184,7 +184,7 @@ type defined in the spec::
>>> from datetime import datetime
>>> cal.add('dtstart', datetime(2005,4,4,8,0,0))
>>> cal['dtstart'].to_ical()
'20050404T080000'
b'20050404T080000'

If that doesn't work satisfactorily for some reason, you can also do it
manually.
Expand All @@ -197,7 +197,7 @@ So if you want to do it manually::
>>> from icalendar import vDatetime
>>> now = datetime(2005,4,4,8,0,0)
>>> vDatetime(now).to_ical()
'20050404T080000'
b'20050404T080000'

So the drill is to initialise the object with a python built in type,
and then call the "to_ical()" method on the object. That will return an
Expand All @@ -220,7 +220,7 @@ value directly::
>>> cal = Calendar()
>>> cal.add('dtstart', datetime(2005,4,4,8,0,0))
>>> cal['dtstart'].to_ical()
'20050404T080000'
b'20050404T080000'
>>> cal.decoded('dtstart')
datetime.datetime(2005, 4, 4, 8, 0)

Expand All @@ -232,12 +232,13 @@ Property parameters are automatically added, depending on the input value. For
example, for date/time related properties, the value type and timezone
identifier (if applicable) are automatically added here::

>>> import pytz
>>> event = Event()
>>> event.add('dtstart', datetime(2010, 10, 10, 10, 0, 0,
... tzinfo=pytz.timezone("Europe/Vienna")))

>>> lines = event.to_ical().splitlines()
>>> self.assertTrue(
>>> assert (
... b"DTSTART;TZID=Europe/Vienna;VALUE=DATE-TIME:20101010T100000"
... in lines)

Expand All @@ -247,9 +248,9 @@ dictionary to the add method like so::

>>> event = Event()
>>> event.add('X-TEST-PROP', 'tryout.',
.... parameters={'prop1': 'val1', 'prop2': 'val2'})
... parameters={'prop1':'val1', 'prop2':'val2'})
>>> lines = event.to_ical().splitlines()
>>> self.assertTrue(b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines)
>>> assert b"X-TEST-PROP;PROP1=val1;PROP2=val2:tryout." in lines


Example
Expand All @@ -270,7 +271,6 @@ Some properties are required to be compliant::

We need at least one subcomponent for a calendar to be compliant::

>>> import pytz
>>> event = Event()
>>> event.add('summary', 'Python meeting about calendaring')
>>> event.add('dtstart', datetime(2005,4,4,8,0,0,tzinfo=pytz.utc))
Expand Down Expand Up @@ -314,6 +314,7 @@ Write to disk::
>>> directory = tempfile.mkdtemp()
>>> f = open(os.path.join(directory, 'example.ics'), 'wb')
>>> f.write(cal.to_ical())
570
>>> f.close()


Expand Down
67 changes: 67 additions & 0 deletions src/icalendar/tests/test_with_doctest.py
@@ -0,0 +1,67 @@
"""This file tests the source code provided by the documentation.
See
- doctest documentation: https://docs.python.org/3/library/doctest.html
- Issue 443: https://github.com/collective/icalendar/issues/443
This file should be tests, too:
>>> print("Hello World!")
Hello World!
"""
import doctest
import os
import pytest
import importlib

HERE = os.path.dirname(__file__) or "."
ICALENDAR_PATH = os.path.dirname(HERE)

PYTHON_FILES = [
os.path.join(dirpath, filename)
for dirpath, dirnames, filenames in os.walk(ICALENDAR_PATH)
for filename in filenames if filename.lower().endswith(".py")
]

MODULE_NAMES = [
"icalendar" + python_file[len(ICALENDAR_PATH):-3].replace("/", ".")
for python_file in PYTHON_FILES
]

def test_this_module_is_among_them():
assert __name__ in MODULE_NAMES

@pytest.mark.parametrize("module_name", MODULE_NAMES)
def test_docstring_of_python_file(module_name):
"""This test runs doctest on the Python module."""
module = importlib.import_module(module_name)
test_result = doctest.testmod(module, name=module_name)
assert test_result.failed == 0, f"{test_result.failed} errors in {module_name}"


# This collection needs to exclude .tox and other subdirectories
DOCUMENTATION_PATH = os.path.join(HERE, "../../../")

DOCUMENT_PATHS = [
os.path.join(DOCUMENTATION_PATH, subdir, filename)
for subdir in ["docs", "."]
for filename in os.listdir(os.path.join(DOCUMENTATION_PATH, subdir))
if filename.lower().endswith(".rst")
]

@pytest.mark.parametrize("filename", [
"README.rst",
"index.rst",
])
def test_files_is_included(filename):
assert any(path.endswith(filename) for path in DOCUMENT_PATHS)

@pytest.mark.parametrize("document", DOCUMENT_PATHS)
def test_documentation_file(document):
"""This test runs doctest on a documentation file."""
test_result = doctest.testfile(document, module_relative=False)
assert test_result.failed == 0, f"{test_result.failed} errors in {os.path.basename(document)}"



1 change: 1 addition & 0 deletions tox.ini
Expand Up @@ -10,6 +10,7 @@ usedevelop=True
deps =
pytest
coverage
hypothesis
commands =
coverage run --source=src/icalendar --omit=*/tests/* --module pytest []
coverage report
Expand Down

0 comments on commit 3e6777d

Please sign in to comment.