Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test .py and .rst files with doctest #445

Merged
merged 8 commits into from Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one should do in the tests_require list that you can find in the setup.py file:

tests_require = []

Then the requirements_docs.txt file should install . [test] (untested)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your reply! Hm I do not know why hypothesis is even in there.

Here, it uses use_debelop=True and therewith does a pip install ..
We do have different tests running that have different dependencies. Using use-develop is what we do here. I think, changing this would go into an other PR as it is independent of doctest. If you think, that change is worth experimenting with, you can create a PR or an issue, I would say. I do not really see a difference at the moment.

What do you think?

commands =
coverage run --source=src/icalendar --omit=*/tests/* --module pytest []
coverage report
Expand Down