Skip to content

Commit 1cdff63

Browse files
committed
add contribution guideline and tests
AI Prompts: claude-sonnet-4-6: <ide_opened_file>The user opened the file ./docs/usage.rst in the IDE. This may or may not be related to the current task.</ide_opened_file> Write basic install instructions for developers in contributing.rst claude-sonnet-4-6: <ide_opened_file>The user opened the file ./README.rst in the IDE. This may or may not be related to the current task.</ide_opened_file> test the main functionality of the package with pytest
1 parent 0a6c3ef commit 1cdff63

File tree

11 files changed

+449
-2
lines changed

11 files changed

+449
-2
lines changed

.claude/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"hooks": {
3+
"UserPromptSubmit": [
4+
{
5+
"hooks": [
6+
{
7+
"id": "ai-prompt-auto-commit",
8+
"type": "command",
9+
"command": "input=$(cat); ts=$(date +%Y-%m-%dT%H-%M-%S); model=$(printf '%s' \"$input\" | jq -r '.model // \"claude-sonnet-4-6\"'); printf '%s' \"$input\" | jq -r '.prompt' > \".prompts/${ts}_${model}.md\""
10+
}
11+
]
12+
}
13+
]
14+
}
15+
}

.github/workflows/tests.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,26 @@ jobs:
2222

2323
- name: Create documentation
2424
run: make html
25-
25+
26+
test:
27+
runs-on: ubuntu-latest
28+
strategy:
29+
matrix:
30+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
31+
32+
steps:
33+
- uses: actions/checkout@v4
34+
35+
- uses: actions/setup-python@v5
36+
with:
37+
python-version: ${{ matrix.python-version }}
38+
39+
- name: Install package and test dependencies
40+
run: make venv
41+
42+
- name: Run tests
43+
run: make test
44+
2645
test-distribution:
2746
name: Check built package
2847
runs-on: ubuntu-latest
@@ -46,6 +65,7 @@ jobs:
4665
needs:
4766
- build-docs
4867
- test-distribution
68+
- test
4969
if: startsWith(github.ref, 'refs/tags/v')
5070
runs-on: ubuntu-latest
5171
environment: PyPI

CONTRIBUTING.md

Whitespace-only changes.

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ venv: $(VENV)/bin/activate
2020

2121
# ── docs ───────────────────────────────────────────────────────────────────
2222

23-
.PHONY: html livehtml clean help clean-python clean-all
23+
.PHONY: html livehtml clean help clean-python clean-all test
2424

2525
html: $(VENV)/bin/activate
2626
$(SPHINXBUILD) -M html $(SOURCEDIR) $(BUILDDIR) $(SPHINXOPTS)
@@ -38,3 +38,6 @@ clean-all: clean clean-python
3838

3939
help: $(VENV)/bin/activate
4040
$(SPHINXBUILD) -M help $(SOURCEDIR) $(BUILDDIR) $(SPHINXOPTS)
41+
42+
test: $(VENV)/bin/activate
43+
pytest

docs/contributing.rst

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
Contributing
2+
============
3+
4+
This page describes how to set up a local development environment.
5+
6+
Prerequisites
7+
-------------
8+
9+
- Python 3.10 or later
10+
- Git
11+
- ``make``
12+
13+
Getting started
14+
---------------
15+
16+
Clone the repository:
17+
18+
.. code-block:: bash
19+
20+
git clone https://github.com/niccokunzmann/sphinx-icalendar-extension.git
21+
cd sphinx-icalendar-extension
22+
23+
Create a virtual environment and install the package with its development
24+
dependencies in editable mode:
25+
26+
.. code-block:: bash
27+
28+
make venv
29+
30+
This runs ``python3 -m venv .venv`` and then ``pip install -e ".[dev]"``,
31+
which installs ``pytest``, ``sphinx``, and ``sphinx-autobuild`` alongside the
32+
extension itself.
33+
34+
Building the docs
35+
-----------------
36+
37+
Build the HTML documentation once:
38+
39+
.. code-block:: bash
40+
41+
make html
42+
43+
The output is written to ``docs/_build/html/``. Open
44+
``docs/_build/html/index.html`` in a browser to review the result.
45+
46+
For a live-reloading preview while editing:
47+
48+
.. code-block:: bash
49+
50+
make livehtml
51+
52+
Running the tests
53+
-----------------
54+
55+
.. code-block:: bash
56+
57+
make test
58+
59+
Cleaning up
60+
-----------
61+
62+
Remove the built docs:
63+
64+
.. code-block:: bash
65+
66+
make clean
67+
68+
Remove the virtual environment as well:
69+
70+
.. code-block:: bash
71+
72+
make clean-all

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ A minimal Sphinx extension that renders ``.. code-block:: calendar`` directives
1010
install
1111
usage
1212
changelog
13+
contributing

tests/__init__.py

Whitespace-only changes.

tests/test_lexer.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Tests for sphinx_icalendar._lexer (ICalendarLexer, JCalLexer)."""
2+
3+
from __future__ import annotations
4+
5+
from pygments.token import Token
6+
7+
from sphinx_icalendar._lexer import ICalendarLexer, JCalLexer
8+
9+
10+
# ── ICalendarLexer ────────────────────────────────────────────────────────────
11+
12+
13+
def test_icalendar_lexer_aliases():
14+
assert "ics" in ICalendarLexer.aliases
15+
assert "icalendar" in ICalendarLexer.aliases
16+
17+
18+
def test_icalendar_lexer_filenames():
19+
assert "*.ics" in ICalendarLexer.filenames
20+
21+
22+
ICS_SNIPPET = """\
23+
BEGIN:VCALENDAR
24+
DTSTART:20260301T090000Z
25+
SUMMARY:Hello world
26+
END:VCALENDAR
27+
"""
28+
29+
30+
def test_icalendar_lexer_tokenises_begin_end():
31+
lexer = ICalendarLexer()
32+
tokens = list(lexer.get_tokens(ICS_SNIPPET))
33+
token_types = {t for t, _ in tokens}
34+
# BEGIN/END lines → Name.Tag
35+
assert Token.Name.Tag in token_types
36+
37+
38+
def test_icalendar_lexer_tokenises_property_name():
39+
lexer = ICalendarLexer()
40+
tokens = list(lexer.get_tokens(ICS_SNIPPET))
41+
token_types = {t for t, _ in tokens}
42+
# DTSTART → Keyword
43+
assert Token.Keyword in token_types
44+
45+
46+
def test_icalendar_lexer_tokenises_datetime():
47+
lexer = ICalendarLexer()
48+
tokens = list(lexer.get_tokens(ICS_SNIPPET))
49+
token_types = {t for t, _ in tokens}
50+
assert Token.Literal.Date in token_types
51+
52+
53+
# ── JCalLexer ─────────────────────────────────────────────────────────────────
54+
55+
56+
def test_jcal_lexer_aliases():
57+
assert "jcal" in JCalLexer.aliases
58+
assert "jcalendar" in JCalLexer.aliases
59+
60+
61+
def test_jcal_lexer_produces_tokens():
62+
lexer = JCalLexer()
63+
tokens = list(lexer.get_tokens('["vcalendar", [], []]'))
64+
assert tokens # non-empty

tests/test_setup.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""Tests for sphinx_icalendar._setup (get_lexers, setup metadata)."""
2+
3+
from __future__ import annotations
4+
5+
from sphinx_icalendar._lexer import ICalendarLexer, JCalLexer
6+
from sphinx_icalendar._setup import get_lexers
7+
8+
9+
def test_get_lexers_returns_all_aliases():
10+
lexers = get_lexers()
11+
assert set(lexers) == {"ics", "icalendar", "jcal", "jcalendar"}
12+
13+
14+
def test_get_lexers_ics_is_icalendar_lexer():
15+
assert get_lexers()["ics"] is ICalendarLexer
16+
17+
18+
def test_get_lexers_icalendar_is_icalendar_lexer():
19+
assert get_lexers()["icalendar"] is ICalendarLexer
20+
21+
22+
def test_get_lexers_jcal_is_jcal_lexer():
23+
assert get_lexers()["jcal"] is JCalLexer
24+
25+
26+
def test_get_lexers_jcalendar_is_jcal_lexer():
27+
assert get_lexers()["jcalendar"] is JCalLexer

tests/test_transform.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Tests for sphinx_icalendar._transform (CalendarTransform)."""
2+
3+
from __future__ import annotations
4+
5+
from docutils import nodes
6+
from docutils.frontend import OptionParser
7+
from docutils.parsers.rst import Parser
8+
from docutils.utils import new_document
9+
10+
from sphinx_icalendar._nodes import calendar_block
11+
from sphinx_icalendar._transform import CalendarTransform
12+
13+
14+
ICS_SOURCE = """\
15+
BEGIN:VCALENDAR
16+
VERSION:2.0
17+
PRODID:-//test//EN
18+
BEGIN:VEVENT
19+
SUMMARY:Demo event
20+
DTSTART:20260301T090000Z
21+
DTEND:20260301T100000Z
22+
UID:demo-001@test
23+
END:VEVENT
24+
END:VCALENDAR
25+
"""
26+
27+
28+
def _make_document() -> nodes.document:
29+
settings = OptionParser(components=(Parser,)).get_default_values()
30+
return new_document("<test>", settings)
31+
32+
33+
def _apply_transform(document: nodes.document) -> None:
34+
transform = CalendarTransform(document)
35+
transform.apply()
36+
37+
38+
def test_calendar_block_replaces_calendar_code_block():
39+
document = _make_document()
40+
block = nodes.literal_block(ICS_SOURCE, ICS_SOURCE)
41+
block["language"] = "calendar"
42+
document += block
43+
44+
_apply_transform(document)
45+
46+
result = list(document.findall(calendar_block))
47+
assert len(result) == 1
48+
assert result[0]["ical_source"] == ICS_SOURCE
49+
50+
51+
def test_non_calendar_code_block_is_left_alone():
52+
document = _make_document()
53+
block = nodes.literal_block("print('hi')", "print('hi')")
54+
block["language"] = "python"
55+
document += block
56+
57+
_apply_transform(document)
58+
59+
assert not list(document.findall(calendar_block))
60+
assert len(list(document.findall(nodes.literal_block))) == 1
61+
62+
63+
def test_calendar_block_stores_ical_source():
64+
document = _make_document()
65+
block = nodes.literal_block(ICS_SOURCE, ICS_SOURCE)
66+
block["language"] = "calendar"
67+
document += block
68+
69+
_apply_transform(document)
70+
71+
[node] = document.findall(calendar_block)
72+
assert node["ical_source"] == ICS_SOURCE
73+
74+
75+
def test_multiple_calendar_blocks_all_replaced():
76+
document = _make_document()
77+
for _ in range(3):
78+
block = nodes.literal_block(ICS_SOURCE, ICS_SOURCE)
79+
block["language"] = "calendar"
80+
document += block
81+
82+
_apply_transform(document)
83+
84+
assert len(list(document.findall(calendar_block))) == 3
85+
assert not list(document.findall(nodes.literal_block))

0 commit comments

Comments
 (0)