From 979f263fcd023b2ba31d133fbdc9fc55498f5f04 Mon Sep 17 00:00:00 2001 From: Johan Hidding Date: Wed, 5 Mar 2025 07:28:27 +0100 Subject: [PATCH 1/6] enable testing on windows and macos --- .github/workflows/python-package.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5360337..4adb05f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -12,12 +12,14 @@ on: jobs: build: - runs-on: ubuntu-latest strategy: fail-fast: false matrix: + os: ["windows-latest", "ubuntu-latest", "macos-latest"] python-version: ["3.11", "3.12", "3.13"] + runs-on: ${{matrix.os}} + steps: - uses: actions/checkout@v3 - name: Install uv From aa4bfb98701c99700c05b124cd2dd3a25707fae2 Mon Sep 17 00:00:00 2001 From: Johan Hidding Date: Wed, 5 Mar 2025 07:41:00 +0100 Subject: [PATCH 2/6] explicitly run sys.executable instead of python in unittest (windows doesnt pick up on virtualenv) --- test/test_typst.py | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_typst.py b/test/test_typst.py index a0ad487..0b63ca3 100644 --- a/test/test_typst.py +++ b/test/test_typst.py @@ -5,6 +5,6 @@ def test_typst(tmp_path: Path, data: Path): copytree(data / "typst", tmp_path / "typst") - run(["python", "-m", "entangled.main", "tangle"], + run([sys.executable, "-m", "entangled.main", "tangle"], cwd=tmp_path / "typst", check=True) assert (tmp_path / "typst" / "fib.py").exists() diff --git a/uv.lock b/uv.lock index 2ba9665..ccc8d41 100644 --- a/uv.lock +++ b/uv.lock @@ -210,7 +210,7 @@ wheels = [ [[package]] name = "entangled-cli" -version = "2.1.10" +version = "2.1.11" source = { editable = "." } dependencies = [ { name = "argh" }, From 5bb9edd23bab8328bda024ba2c6b5a4f4d598c7b Mon Sep 17 00:00:00 2001 From: Johan Hidding Date: Wed, 5 Mar 2025 07:43:54 +0100 Subject: [PATCH 3/6] disable py313 on windows --- .github/workflows/python-package.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4adb05f..366c322 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,6 +17,9 @@ jobs: matrix: os: ["windows-latest", "ubuntu-latest", "macos-latest"] python-version: ["3.11", "3.12", "3.13"] + exclude: + - os: "windows-latest" + python-version: "3.13" runs-on: ${{matrix.os}} From 2f4941d0e5dfa9537e8071e9fa0b6bc34e9d56c9 Mon Sep 17 00:00:00 2001 From: Johan Hidding Date: Wed, 5 Mar 2025 07:45:12 +0100 Subject: [PATCH 4/6] actually import sys in typst test --- test/test_typst.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/test_typst.py b/test/test_typst.py index 0b63ca3..a263a39 100644 --- a/test/test_typst.py +++ b/test/test_typst.py @@ -2,6 +2,8 @@ from subprocess import run from pathlib import Path from shutil import copytree +import sys + def test_typst(tmp_path: Path, data: Path): copytree(data / "typst", tmp_path / "typst") From 49b1813c9d3e3b3300fd62e27f81634360d97c21 Mon Sep 17 00:00:00 2001 From: Johan Hidding Date: Wed, 5 Mar 2025 11:39:44 +0100 Subject: [PATCH 5/6] test os interop with similar to hello-world test, but with paths --- test/data/os_interop/.gitignore | 4 + test/data/os_interop/Makefile | 24 ++++ test/data/os_interop/doc/index.md | 110 +++++++++++++++++++ test/data/os_interop/src/euler_number.c | 17 +++ test/data/os_interop/src/euler_number.c.edit | 17 +++ test/test_os_interop.py | 23 ++++ 6 files changed, 195 insertions(+) create mode 100644 test/data/os_interop/.gitignore create mode 100644 test/data/os_interop/Makefile create mode 100644 test/data/os_interop/doc/index.md create mode 100644 test/data/os_interop/src/euler_number.c create mode 100644 test/data/os_interop/src/euler_number.c.edit create mode 100644 test/test_os_interop.py diff --git a/test/data/os_interop/.gitignore b/test/data/os_interop/.gitignore new file mode 100644 index 0000000..8dca1de --- /dev/null +++ b/test/data/os_interop/.gitignore @@ -0,0 +1,4 @@ +build +.entangled +euler + diff --git a/test/data/os_interop/Makefile b/test/data/os_interop/Makefile new file mode 100644 index 0000000..605fdcc --- /dev/null +++ b/test/data/os_interop/Makefile @@ -0,0 +1,24 @@ +# ~/~ begin <>[init] +.RECIPEPREFIX = > +.PHONY: clean + +build_dir = ./build +source_files = src/euler_number.cc + +obj_files = $(source_files:%.cc=$(build_dir)/%.o) +dep_files = $(obj_files:%.o=%.d) + +euler: $(obj_files) +> @echo -e "Linking \e[32;1m$@\e[m" +> @gcc $^ -o $@ + +$(build_dir)/%.o: %.c +> @echo -e "Compiling \e[33m$@\e[m" +> @mkdir -p $(@D) +> @gcc -MMD -c $< -o $@ + +clean: +> rm -rf build euler + +-include $(dep_files) +# ~/~ end diff --git a/test/data/os_interop/doc/index.md b/test/data/os_interop/doc/index.md new file mode 100644 index 0000000..3b171c1 --- /dev/null +++ b/test/data/os_interop/doc/index.md @@ -0,0 +1,110 @@ +--- +title: Testing Windows/Linux interop +subtitle: a story of a mangled path +author: Johan Hidding +--- + +Paths on Windows are spelled with backslashes in between them, for example: + +``` +C:\Program Files\I have no clue\WhatImDoing.exe +``` + +On UNIXes (Linux and to and extend MacOS), paths are separated using slashes: + +``` +/usr/share/doc/man-pages/still-have-no-clue +``` + +Entangled uses Python's `Path` API throughout to deal with this difference transparently. Paths find a representation in the comment annotations left in tangled files, so that the markdown source of a piece of code can be found. We want consistent behaviour when using Entangled between different OS, so all paths should be encoded UNIX style. + +This test creates a source file in a different path from this markdown source. + +## Euler's number + +We'll compute Euler's number in C. Euler's number is the exponential base for which the differential equation, + +$$y_t = y,$$ + +holds. With a Taylor expansion we may see that the constant $e$ in the solution $A e^t$ can be computed as, + +$$e = \sum_{n=0}^{\infty} \frac{1}{n!} = 1 + \frac{1}{1!} + \frac{1}{2!} + \dots$$ + +In C, we can compute this number, here to the 9th term: + +``` {.c #series-expansion} +double euler_number = 1.0; +int factorial = 1; +for (int i = 1; i < 10; ++i) { + factorial *= i; + euler_number += 1.0 / factorial; +} +``` + +Wrapping this in an example program: + +``` {.c file=src/euler_number.c} +#include +#include + +int main() { + <> + printf("Euler's number e = %e\n", euler_number); + return EXIT_SUCCESS; +} +``` + +To build this program, the following `Makefile` can be used: + +``` {.make file=Makefile} +.RECIPEPREFIX = > +.PHONY: clean + +build_dir = ./build +source_files = src/euler_number.cc + +obj_files = $(source_files:%.cc=$(build_dir)/%.o) +dep_files = $(obj_files:%.o=%.d) + +euler: $(obj_files) +> @echo -e "Linking \e[32;1m$@\e[m" +> @gcc $^ -o $@ + +$(build_dir)/%.o: %.c +> @echo -e "Compiling \e[33m$@\e[m" +> @mkdir -p $(@D) +> @gcc -MMD -c $< -o $@ + +clean: +> rm -rf build euler + +-include $(dep_files) +``` + +I know, overkill. + +## Expected output + +When we tangle this program, this looks like: + +```c +/* ~/~ begin <>[init] */ +#include +#include + +int main() { + /* ~/~ begin <>[init] */ + double euler_number = 1.0; + int factorial = 1; + for (int i = 1; i < 10; ++i) { + factorial *= i; + euler_number += 1.0 / factorial; + } + /* ~/~ end */ + printf("Euler's number e = %e\n", euler_number); + return EXIT_SUCCESS; +} +/* ~/~ end */ +``` + +Notice, the forward slashes in the paths. diff --git a/test/data/os_interop/src/euler_number.c b/test/data/os_interop/src/euler_number.c new file mode 100644 index 0000000..6213772 --- /dev/null +++ b/test/data/os_interop/src/euler_number.c @@ -0,0 +1,17 @@ +/* ~/~ begin <>[init] */ +#include +#include + +int main() { + /* ~/~ begin <>[init] */ + double euler_number = 1.0; + int factorial = 1; + for (int i = 1; i < 10; ++i) { + factorial *= i; + euler_number += 1.0 / factorial; + } + /* ~/~ end */ + printf("Euler's number e = %e\n", euler_number); + return EXIT_SUCCESS; +} +/* ~/~ end */ diff --git a/test/data/os_interop/src/euler_number.c.edit b/test/data/os_interop/src/euler_number.c.edit new file mode 100644 index 0000000..2a1530a --- /dev/null +++ b/test/data/os_interop/src/euler_number.c.edit @@ -0,0 +1,17 @@ +/* ~/~ begin <>[init] */ +#include +#include + +int main() { + /* ~/~ begin <>[init] */ + double euler_number = 1.0; + int factorial = 1; + for (int i = 1; i < 20; ++i) { + factorial *= i; + euler_number += 1.0 / factorial; + } + /* ~/~ end */ + printf("Euler's number e = %e\n", euler_number); + return EXIT_SUCCESS; +} +/* ~/~ end */ diff --git a/test/test_os_interop.py b/test/test_os_interop.py new file mode 100644 index 0000000..f00b811 --- /dev/null +++ b/test/test_os_interop.py @@ -0,0 +1,23 @@ +from entangled.markdown_reader import read_markdown_file +from entangled.tangle import tangle_ref +from entangled.code_reader import CodeReader + +from pathlib import Path +import os +from shutil import copytree, move +from contextlib import chdir + + +def test_tangle_ref(data, tmp_path): + copytree(data / "os_interop", tmp_path / "os_interop") + with chdir(tmp_path / "os_interop"): + refs, _ = read_markdown_file(Path("doc/index.md")) + tangled, deps = tangle_ref(refs, "src/euler_number.c") + assert deps == {"doc/index.md"} + with open("src/euler_number.c", "r") as f: + assert f.read().strip() == tangled.strip() + + cb_old = next(refs["series-expansion"]).source + cr = CodeReader("-", refs).run(Path("src/euler_number.c.edit").read_text()) + cb_new = next(refs["series-expansion"]).source + assert cb_old != cb_new From bdd86a3ac3b5eca27e167bf53d60cb2c579e64ed Mon Sep 17 00:00:00 2001 From: Johan Hidding Date: Wed, 5 Mar 2025 12:17:30 +0100 Subject: [PATCH 6/6] represent all paths internally as PurePath --- entangled/code_reader.py | 8 ++++---- entangled/commands/stitch.py | 2 +- entangled/document.py | 5 +++-- entangled/markdown_reader.py | 12 ++++++------ entangled/tangle.py | 2 +- entangled/text_location.py | 3 ++- test/test_os_interop.py | 4 ++-- test/test_tangle.py | 4 ++-- 8 files changed, 21 insertions(+), 19 deletions(-) diff --git a/entangled/code_reader.py b/entangled/code_reader.py index bd1af73..3e350e1 100644 --- a/entangled/code_reader.py +++ b/entangled/code_reader.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from pathlib import Path +from pathlib import Path, PurePath import mawk import re @@ -18,9 +18,9 @@ class Frame: class CodeReader(mawk.RuleSet): """Reads an annotated code file.""" - def __init__(self, path: str, refs: ReferenceMap): + def __init__(self, path: PurePath, refs: ReferenceMap): self.location = TextLocation(path, 0) - self.stack: list[Frame] = [Frame(ReferenceId("#root#", "", -1), "")] + self.stack: list[Frame] = [Frame(ReferenceId("#root#", PurePath("-"), -1), "")] self.refs: ReferenceMap = refs @property @@ -56,7 +56,7 @@ def on_block_begin(self, m: re.Match): self.stack.append( Frame( - ReferenceId(m["ref_name"], m["source"], ref_count), m["indent"], content + ReferenceId(m["ref_name"], PurePath(m["source"]), ref_count), m["indent"], content ) ) return [] diff --git a/entangled/commands/stitch.py b/entangled/commands/stitch.py index c95ee58..e879b04 100644 --- a/entangled/commands/stitch.py +++ b/entangled/commands/stitch.py @@ -57,7 +57,7 @@ def stitch(*, force: bool = False, show: bool = False): logging.debug("reading `%s`", path) t.update(path) with open(path, "r") as f: - CodeReader(str(path), refs).run(f.read()) + CodeReader(path, refs).run(f.read()) for path in input_file_list: t.write(path, stitch_markdown(refs, content[path]), []) diff --git a/entangled/document.py b/entangled/document.py index 8205091..94316d1 100644 --- a/entangled/document.py +++ b/entangled/document.py @@ -3,6 +3,7 @@ from collections import defaultdict from functools import singledispatchmethod from itertools import chain +from pathlib import PurePath from .config import Language, AnnotationMethod, config from .properties import Property, get_attribute @@ -17,7 +18,7 @@ def length(iter: Iterable[Any]) -> int: @dataclass class ReferenceId: name: str - file: str + file: PurePath ref_count: int def __hash__(self): @@ -71,7 +72,7 @@ def by_name(self, n: str) -> Iterable[CodeBlock]: return (self.map[r] for r in self.index[n]) - def new_id(self, filename: str, name: str) -> ReferenceId: + def new_id(self, filename: PurePath, name: str) -> ReferenceId: c = length(filter(lambda r: r.file == filename, self.index[name])) return ReferenceId(name, filename, c) diff --git a/entangled/markdown_reader.py b/entangled/markdown_reader.py index bfa9b06..f5f16d2 100644 --- a/entangled/markdown_reader.py +++ b/entangled/markdown_reader.py @@ -1,6 +1,6 @@ from typing import Optional from copy import copy -from pathlib import Path +from pathlib import Path, PurePath import re import mawk @@ -20,8 +20,8 @@ class MarkdownLexer(mawk.RuleSet): content.""" def __init__( self, - filename: str - ): + filename: Path + ): self.location = TextLocation(filename) self.raw_content: list[RawContent] = [] self.inside_codeblock: bool = False @@ -122,13 +122,13 @@ def read_markdown_file( -> tuple[ReferenceMap, list[Content]]: with open(path, "r") as f: - path_str = str(path.resolve().relative_to(Path.cwd())) - return read_markdown_string(f.read(), path_str, refs, hooks) + rel_path = path.resolve().relative_to(Path.cwd()) + return read_markdown_string(f.read(), rel_path, refs, hooks) def read_markdown_string( text: str, - path_str: str = "-", + path_str: Path = Path("-"), refs: ReferenceMap | None = None, hooks: list[HookBase] | None = None) \ -> tuple[ReferenceMap, list[Content]]: diff --git a/entangled/tangle.py b/entangled/tangle.py index c2d11d6..ceff2ed 100644 --- a/entangled/tangle.py +++ b/entangled/tangle.py @@ -95,7 +95,7 @@ def on_begin(self): if self.cb.header is not None: result.append(self.cb.header) result.append( - f"{self.cb.language.comment.open} ~/~ begin <<{self.ref.file}#{self.ref.name}>>[{count}]{self.close_comment}" + f"{self.cb.language.comment.open} ~/~ begin <<{self.ref.file.as_posix()}#{self.ref.name}>>[{count}]{self.close_comment}" ) return result diff --git a/entangled/text_location.py b/entangled/text_location.py index 21b597c..ec7a9e2 100644 --- a/entangled/text_location.py +++ b/entangled/text_location.py @@ -1,9 +1,10 @@ from dataclasses import dataclass +from pathlib import PurePath @dataclass class TextLocation: - filename: str + filename: PurePath line_number: int = 0 def __str__(self): diff --git a/test/test_os_interop.py b/test/test_os_interop.py index f00b811..e68031b 100644 --- a/test/test_os_interop.py +++ b/test/test_os_interop.py @@ -2,7 +2,7 @@ from entangled.tangle import tangle_ref from entangled.code_reader import CodeReader -from pathlib import Path +from pathlib import Path, PurePath import os from shutil import copytree, move from contextlib import chdir @@ -13,7 +13,7 @@ def test_tangle_ref(data, tmp_path): with chdir(tmp_path / "os_interop"): refs, _ = read_markdown_file(Path("doc/index.md")) tangled, deps = tangle_ref(refs, "src/euler_number.c") - assert deps == {"doc/index.md"} + assert deps == {PurePath("doc/index.md")} with open("src/euler_number.c", "r") as f: assert f.read().strip() == tangled.strip() diff --git a/test/test_tangle.py b/test/test_tangle.py index ab2e25a..c635756 100644 --- a/test/test_tangle.py +++ b/test/test_tangle.py @@ -1,7 +1,7 @@ from entangled.markdown_reader import read_markdown_file from entangled.tangle import tangle_ref from entangled.code_reader import CodeReader -from pathlib import Path +from pathlib import Path, PurePath import os from shutil import copytree, move from contextlib import chdir @@ -12,7 +12,7 @@ def test_tangle_ref(data, tmp_path): with chdir(tmp_path / "hello-world"): refs, _ = read_markdown_file(Path("hello-world.md")) tangled, deps = tangle_ref(refs, "hello_world.cc") - assert deps == {"hello-world.md"} + assert deps == {PurePath("hello-world.md")} with open("hello_world.cc", "r") as f: assert f.read() == tangled