Document language for dates; @eval pathname resolution and :cwd (from dev bc7cd62b6f68)
@date/@datetime gain :lang (German: "16. Juni 1910") over a document-wide Language state variable, and :number for the numeric form (en 6/16/1910, de 16.06.1910 per DIN 5008). @eval finds Python modules next to the file that names them regardless of the cwd, and the new :cwd option runs an eval in a chosen working directory (@source_file uses it to resolve against the document). Two new test suites ship in tst/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,7 @@ are regenerated on each release — patches cannot be merged directly.
|
|||||||
Report problems (or send patches) to the author; accepted changes are
|
Report problems (or send patches) to the author; accepted changes are
|
||||||
applied to the development tree and appear in a following snapshot.
|
applied to the development tree and appear in a following snapshot.
|
||||||
|
|
||||||
This snapshot was assembled from development commit `f84517152b7f`.
|
This snapshot was assembled from development commit `bc7cd62b6f68`.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
65
mac/eval.cpp
65
mac/eval.cpp
@@ -7,6 +7,7 @@
|
|||||||
#include "katom.h"
|
#include "katom.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <optional>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
std::string shell(State state, std::string command, Locator loc)
|
std::string shell(State state, std::string command, Locator loc)
|
||||||
@@ -114,12 +115,64 @@ void check_cpp_arguments(katom_list args, Locator loc)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Save the process working directory, change to DIR, and restore on
|
||||||
|
// destruction (exception-safe), so an @eval's :cwd cannot leak into the
|
||||||
|
// rest of the run. NOTE: the cwd is process-global state; if input files
|
||||||
|
// are ever processed in parallel, this needs rethinking.
|
||||||
|
class Cwd_guard
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit Cwd_guard(const std::string& dir)
|
||||||
|
: m_saved(fs::current_path())
|
||||||
|
{
|
||||||
|
fs::current_path(dir);
|
||||||
|
}
|
||||||
|
~Cwd_guard()
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
fs::current_path(m_saved, ec); // never throw from a destructor
|
||||||
|
}
|
||||||
|
Cwd_guard(const Cwd_guard&) = delete;
|
||||||
|
Cwd_guard& operator=(const Cwd_guard&) = delete;
|
||||||
|
private:
|
||||||
|
fs::path m_saved;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
std::string Eval::eval_command(katom_iter begin, katom_iter end)
|
std::string Eval::eval_command(katom_iter begin, katom_iter end)
|
||||||
{
|
{
|
||||||
(void)K::log(3, *begin, *(end - 1));
|
(void)K::log(3, *begin, *(end - 1));
|
||||||
//msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n";
|
//msg() << "in Eval::eval:\n" << ktype << kall << kindex << std::pair(begin, end) << "\n";
|
||||||
katom_iter first = after_whitespace(begin + 1);
|
katom_iter first = after_whitespace(begin + 1);
|
||||||
std::string eval_result = "[unevaluated]";
|
std::string eval_result = "[unevaluated]";
|
||||||
|
|
||||||
|
// :cwd DIR — run the eval (any mode) with DIR as the working directory,
|
||||||
|
// restoring the process cwd afterwards. The default is the directory
|
||||||
|
// ktext was started in (unchanged behavior). DIR may hold state
|
||||||
|
// substitutions (:cwd *K_input_dir* is the document's directory) and,
|
||||||
|
// per the filenames-with-spaces convention, whitespace-separated tokens
|
||||||
|
// are joined until they name an existing directory.
|
||||||
|
std::optional<Cwd_guard> cwd_guard {};
|
||||||
|
if (first->m_text == ":cwd") {
|
||||||
|
katom_iter tok = after_whitespace(first + 1);
|
||||||
|
std::string dir {};
|
||||||
|
katom_iter cursor = tok;
|
||||||
|
katom_iter resume = tok;
|
||||||
|
while (cursor < end - 1 && !cursor->m_text.starts_with(":")) {
|
||||||
|
dir = m_machine.m_state.subst(as_string(tok, cursor + 1, true));
|
||||||
|
resume = after_whitespace(cursor + 1);
|
||||||
|
if (fs::is_directory(dir)) break;
|
||||||
|
cursor = resume;
|
||||||
|
}
|
||||||
|
if (dir.empty() || !fs::is_directory(dir)) {
|
||||||
|
throw Argument_error(
|
||||||
|
"The :cwd directory does not exist: \"" + dir + "\"",
|
||||||
|
begin->m_loc, false);
|
||||||
|
}
|
||||||
|
cwd_guard.emplace(dir);
|
||||||
|
first = resume;
|
||||||
|
}
|
||||||
|
|
||||||
std::string first_word = first->m_text;
|
std::string first_word = first->m_text;
|
||||||
int offset = first_word[0] == ':' ? 1 : 0;
|
int offset = first_word[0] == ':' ? 1 : 0;
|
||||||
std::string command = as_string(first + offset, end - 1, true);
|
std::string command = as_string(first + offset, end - 1, true);
|
||||||
@@ -145,6 +198,18 @@ std::string Eval::eval_command(katom_iter begin, katom_iter end)
|
|||||||
lib_text = string_replace(lib_text, "*KLAMMERTEXT_HOME*", khome);
|
lib_text = string_replace(lib_text, "*KLAMMERTEXT_HOME*", khome);
|
||||||
}
|
}
|
||||||
fs::path libpath(lib_text + ".so");
|
fs::path libpath(lib_text + ".so");
|
||||||
|
// A relative library name not found from the cwd is searched in the
|
||||||
|
// directories of the files the Machine has read (same rule as the
|
||||||
|
// Python module path: the library lives next to the file using it).
|
||||||
|
if (libpath.is_relative() && !fs::exists(fs::absolute(libpath))) {
|
||||||
|
for (const auto& dir : m_machine.m_state.m_search_dirs) {
|
||||||
|
fs::path candidate = fs::path(dir) / libpath;
|
||||||
|
if (fs::exists(candidate)) {
|
||||||
|
libpath = candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
libpath = fs::absolute(libpath);
|
libpath = fs::absolute(libpath);
|
||||||
std::string funcname = args.size() == 4 ? libpath.stem().string() : args[3].m_text;
|
std::string funcname = args.size() == 4 ? libpath.stem().string() : args[3].m_text;
|
||||||
Eval_cpp E_cpp(m_machine, begin->m_loc);
|
Eval_cpp E_cpp(m_machine, begin->m_loc);
|
||||||
|
|||||||
@@ -269,6 +269,9 @@ katom_list Machine::process(
|
|||||||
void Machine::read(const fs::path& pathname)
|
void Machine::read(const fs::path& pathname)
|
||||||
{
|
{
|
||||||
(void)K::log(3, pathname.string());
|
(void)K::log(3, pathname.string());
|
||||||
|
// A file's directory joins the @eval search path (Python modules and
|
||||||
|
// :cpp libraries live next to the file that uses them).
|
||||||
|
m_state.add_search_dir(fs::absolute(pathname).parent_path().string());
|
||||||
m_state.open_frame("Machine state: " + pathname.string());
|
m_state.open_frame("Machine state: " + pathname.string());
|
||||||
std::string text = m_state.subst(trim_right(string_from_file(pathname)));
|
std::string text = m_state.subst(trim_right(string_from_file(pathname)));
|
||||||
katom_list katoms = process(text, pathname);
|
katom_list katoms = process(text, pathname);
|
||||||
@@ -328,6 +331,7 @@ void Machine::expand_read_katoms(
|
|||||||
std::for_each(begin, end, mark_as_replaced);
|
std::for_each(begin, end, mark_as_replaced);
|
||||||
|
|
||||||
input_filename = fs::canonical(input_filename);
|
input_filename = fs::canonical(input_filename);
|
||||||
|
m_state.add_search_dir(input_filename.parent_path().string());
|
||||||
std::string text = trim_right(string_from_file(input_filename.string()));
|
std::string text = trim_right(string_from_file(input_filename.string()));
|
||||||
katom_list ks = katomize(line_split(text), input_filename);
|
katom_list ks = katomize(line_split(text), input_filename);
|
||||||
// ks =
|
// ks =
|
||||||
|
|||||||
@@ -259,6 +259,16 @@ std::vector<std::string> State::all_names()
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void State::add_search_dir(const std::string& dir)
|
||||||
|
{
|
||||||
|
if (!dir.empty()
|
||||||
|
&& std::find(m_search_dirs.begin(), m_search_dirs.end(), dir)
|
||||||
|
== m_search_dirs.end()) {
|
||||||
|
m_search_dirs.push_back(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string State::python_code()
|
std::string State::python_code()
|
||||||
{
|
{
|
||||||
(void)K::log(3);
|
(void)K::log(3);
|
||||||
@@ -267,7 +277,12 @@ std::string State::python_code()
|
|||||||
std::stringstream ss {};
|
std::stringstream ss {};
|
||||||
|
|
||||||
ss << "import sys\n";
|
ss << "import sys\n";
|
||||||
for (auto d : sks_dirs()) {
|
strings_t python_dirs = sks_dirs();
|
||||||
|
// The directories of the files this Machine has read: a module next to
|
||||||
|
// the file whose @eval names it is found regardless of the cwd.
|
||||||
|
python_dirs.insert(python_dirs.end(),
|
||||||
|
m_search_dirs.begin(), m_search_dirs.end());
|
||||||
|
for (auto d : python_dirs) {
|
||||||
auto python_files = pathnames_with_extension(d, "py");
|
auto python_files = pathnames_with_extension(d, "py");
|
||||||
if (!python_files.empty()) {
|
if (!python_files.empty()) {
|
||||||
ss << "sys.path.append('" << d << "')\n";
|
ss << "sys.path.append('" << d << "')\n";
|
||||||
|
|||||||
@@ -77,10 +77,16 @@ public:
|
|||||||
|
|
||||||
void parse_state_katoms(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, katom_list katoms);
|
void parse_state_katoms(std::vector<Katom>::iterator begin, std::vector<Katom>::iterator end, katom_list katoms);
|
||||||
std::vector<std::string> all_names();
|
std::vector<std::string> all_names();
|
||||||
|
void add_search_dir(const std::string& dir);
|
||||||
std::string python_code();
|
std::string python_code();
|
||||||
std::string describe(bool show_environment=false, int margin_size=2) const;
|
std::string describe(bool show_environment=false, int margin_size=2) const;
|
||||||
|
|
||||||
std::vector<Frame> m_frames {};
|
std::vector<Frame> m_frames {};
|
||||||
|
// Directories of the files the Machine has read (input files, klammer
|
||||||
|
// sets, @read targets), in reading order: @eval finds Python modules
|
||||||
|
// and :cpp libraries next to the file that uses them (see python_code()
|
||||||
|
// and Eval::eval_command).
|
||||||
|
std::vector<std::string> m_search_dirs {};
|
||||||
// @@@state Image_search_path :set :append :replace :argtype :desc
|
// @@@state Image_search_path :set :append :replace :argtype :desc
|
||||||
// Parameter_set m_parameters = Parameter_set("name :set :append :replace :argtype :delim :desc");
|
// Parameter_set m_parameters = Parameter_set("name :set :append :replace :argtype :delim :desc");
|
||||||
Parameter_set m_parameters = Parameter_set("name :value :append :replace :delim :desc");
|
Parameter_set m_parameters = Parameter_set("name :value :append :replace :delim :desc");
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ A word or phrase displayed verbatim in a line
|
|||||||
@@c :: @eval code_format.Code_fragment(K) eval@
|
@@c :: @eval code_format.Code_fragment(K) eval@
|
||||||
@@
|
@@
|
||||||
|
|
||||||
@@source_file filename : @eval code_format.Source(K) @ @@
|
# :cwd makes the filename resolve against the DOCUMENT's directory, not
|
||||||
|
# the directory ktext happens to run in.
|
||||||
|
@@source_file filename : @eval :cwd *K_input_dir* code_format.Source(K) @ @@
|
||||||
|
|
||||||
|
|
||||||
#[
|
#[
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
@@date.k :days.int 0 :
|
@@date.k :days.int 0 :lang.language :number.bool false :
|
||||||
Date formatted as "16 June 1910", offset by ^:days days from today
|
Date formatted as "16 June 1910" (^:lang de: "16. Juni 1910"), offset by
|
||||||
(^:days 1 is tomorrow, ^:days -1 is yesterday)
|
^:days days from today (^:days 1 is tomorrow, ^:days -1 is yesterday).
|
||||||
|
^:lang selects the language of the month name and the date form; without
|
||||||
|
it the document-wide Language state variable applies (default en).
|
||||||
|
^:number true gives the numeric form, which is language-specific in
|
||||||
|
order, separator, and padding: "6/16/1910" (en, month first),
|
||||||
|
"16.06.1910" (de, day first, zero-padded per DIN 5008)
|
||||||
@@
|
@@
|
||||||
@@date.html :: @eval date.date(K) eval@ @@
|
@@date.html :: @eval date.date(K) eval@ @@
|
||||||
@@date.tex :: @eval date.date(K) eval@ @@
|
@@date.tex :: @eval date.date(K) eval@ @@
|
||||||
@@date.txt :: @eval date.date(K) eval@ @@
|
@@date.txt :: @eval date.date(K) eval@ @@
|
||||||
|
|
||||||
@@datetime.k :days.int 0 :
|
@@datetime.k :days.int 0 :lang.language :number.bool false :
|
||||||
Date and time formatted as "16 June 1910, 13:10", offset by ^:days days
|
Date and time formatted as "16 June 1910, 13:10" (^:lang de:
|
||||||
from today (^:days 1 is tomorrow, ^:days -1 is yesterday)
|
"16. Juni 1910, 13:10"), offset by ^:days days from today (^:days 1 is
|
||||||
|
tomorrow, ^:days -1 is yesterday). ^:lang and ^:number select the
|
||||||
|
language and the numeric form as for ^@date
|
||||||
@@
|
@@
|
||||||
@@datetime.html :: @eval date.datetime(K) eval@ @@
|
@@datetime.html :: @eval date.datetime(K) eval@ @@
|
||||||
@@datetime.tex :: @eval date.datetime(K) eval@ @@
|
@@datetime.tex :: @eval date.datetime(K) eval@ @@
|
||||||
|
|||||||
@@ -1,17 +1,76 @@
|
|||||||
# Implementation of the @date and @datetime klammers (date.k). Each takes
|
# Implementation of the @date and @datetime klammers (date.k). Each takes
|
||||||
# the current date and time, offset by the :days argument (positive is
|
# the current date and time, offset by the :days argument (positive is
|
||||||
# future, negative is past), and formats it.
|
# future, negative is past), and formats it in the requested language.
|
||||||
|
#
|
||||||
|
# Language selection is two-level: the klammer's :lang argument overrides
|
||||||
|
# the document-wide Language state variable (declared in sks/kutil/kutil.k,
|
||||||
|
# default en; set it for a whole document with @@@state Language :value de).
|
||||||
|
#
|
||||||
|
# :number selects the purely numeric form instead — which is language-
|
||||||
|
# specific in order, separator, and padding: en 5/15/1955 (month first,
|
||||||
|
# unpadded), de 15.05.1955 (day first, zero-padded per DIN 5008).
|
||||||
|
#
|
||||||
|
# Languages are OWN TABLES, deliberately not locales: strftime's %B follows
|
||||||
|
# the process-wide C locale, which must be generated on the host (the
|
||||||
|
# containers ship minimal locale support) and is global mutable state in the
|
||||||
|
# embedded interpreter — the same reason @table's :decimal is implemented by
|
||||||
|
# character translation. Adding a language is adding one LANGUAGES entry:
|
||||||
|
# twelve month names and the date pattern (day/month/year; the day carries
|
||||||
|
# no leading zero). The time suffix ", HH:MM" is language-independent.
|
||||||
|
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
|
|
||||||
|
LANGUAGES = {
|
||||||
|
'en': {
|
||||||
|
'months': ['January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November',
|
||||||
|
'December'],
|
||||||
|
'date': '{day} {month} {year}', # 16 June 1910
|
||||||
|
'number': '{m}/{day}/{year}', # 6/16/1910
|
||||||
|
},
|
||||||
|
'de': {
|
||||||
|
'months': ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
|
||||||
|
'Juli', 'August', 'September', 'Oktober', 'November',
|
||||||
|
'Dezember'],
|
||||||
|
'date': '{day}. {month} {year}', # 16. Juni 1910
|
||||||
|
'number': '{dd}.{mm}.{year}', # 16.06.1910 (DIN 5008)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def offset(days):
|
def offset(days):
|
||||||
return dt.datetime.now() + dt.timedelta(days=days or 0)
|
return dt.datetime.now() + dt.timedelta(days=days or 0)
|
||||||
|
|
||||||
|
|
||||||
|
def language(K):
|
||||||
|
"""The language table for K: :lang argument, else the Language state
|
||||||
|
variable, else English. Returns (table, error_text)."""
|
||||||
|
code = getattr(K, 'lang', '') or getattr(K, 'Language', '') or 'en'
|
||||||
|
if code not in LANGUAGES:
|
||||||
|
return None, ('ERROR: @date/@datetime: unknown language "%s" '
|
||||||
|
'(available: %s)'
|
||||||
|
% (code, ', '.join(sorted(LANGUAGES))))
|
||||||
|
return LANGUAGES[code], None
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(t, lang, number=False):
|
||||||
|
pattern = lang['number'] if number else lang['date']
|
||||||
|
return pattern.format(day=t.day, m=t.month,
|
||||||
|
dd='%02d' % t.day, mm='%02d' % t.month,
|
||||||
|
month=lang['months'][t.month - 1],
|
||||||
|
year=t.year)
|
||||||
|
|
||||||
|
|
||||||
def date(K):
|
def date(K):
|
||||||
return offset(K.days).strftime("%d %B %Y").lstrip("0")
|
lang, error = language(K)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
return format_date(offset(K.days), lang, K.number)
|
||||||
|
|
||||||
|
|
||||||
def datetime(K):
|
def datetime(K):
|
||||||
return offset(K.days).strftime("%d %B %Y, %H:%M").lstrip("0")
|
lang, error = language(K)
|
||||||
|
if error:
|
||||||
|
return error
|
||||||
|
t = offset(K.days)
|
||||||
|
return format_date(t, lang, K.number) + ', %02d:%02d' % (t.hour, t.minute)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class Image(klammer_base.Klammer_base):
|
|||||||
self.basename = klammer_base.unescape_ktesc(self.basename)
|
self.basename = klammer_base.unescape_ktesc(self.basename)
|
||||||
self.source, self.pwidth, self.pheight, self.file_error = self.cache.get(self.K_target, self.basename)
|
self.source, self.pwidth, self.pheight, self.file_error = self.cache.get(self.K_target, self.basename)
|
||||||
if self.file_error:
|
if self.file_error:
|
||||||
self.file_error_message = f'\nERROR: File "{self.K_input_filename}" not found'
|
self.file_error_message = f'\nERROR: Image "{self.basename}" not found'
|
||||||
self.as_string = as_string
|
self.as_string = as_string
|
||||||
if width:
|
if width:
|
||||||
self.width = width
|
self.width = width
|
||||||
@@ -42,7 +42,7 @@ class Image(klammer_base.Klammer_base):
|
|||||||
self.rel_fraction = self.pwidth / self.rel_pwidth
|
self.rel_fraction = self.pwidth / self.rel_pwidth
|
||||||
|
|
||||||
if self.file_error:
|
if self.file_error:
|
||||||
self.file_error_message = f'\nERROR: File "{self.K_input_filename}" not found'
|
self.file_error_message = f'\nERROR: Image "{self.rel}" (the :rel image) not found'
|
||||||
|
|
||||||
def html(self):
|
def html(self):
|
||||||
img_dir = f"{self.K_output_dir}/{self.K_output_basename}/{self.Image_output_dir}"
|
img_dir = f"{self.K_output_dir}/{self.K_output_basename}/{self.Image_output_dir}"
|
||||||
|
|||||||
@@ -80,3 +80,17 @@ An <id> is the value of the ^:id argument for an image.
|
|||||||
:pattern [\s\S]*
|
:pattern [\s\S]*
|
||||||
:python_cast (lambda s: __import__("kutil").filename_list(s))
|
:python_cast (lambda s: __import__("kutil").filename_list(s))
|
||||||
@@@
|
@@@
|
||||||
|
|
||||||
|
@@@argtype language |
|
||||||
|
an ISO 639-1 language code (two lowercase letters) selecting the language
|
||||||
|
of text a klammer generates, e.g. en (English) or de (German). The
|
||||||
|
languages actually available are listed by the klammer that uses the
|
||||||
|
argument (^@date and ^@datetime); an unknown code reports them.
|
||||||
|
:pattern [a-z][a-z]
|
||||||
|
@@@
|
||||||
|
|
||||||
|
# The document-wide language for generated text. A klammer's own :lang
|
||||||
|
# argument overrides it; see the language argtype above. Consumers today:
|
||||||
|
# @date and @datetime (month names and date form). Set it for a whole
|
||||||
|
# document with @@@state Language :value de @@@
|
||||||
|
@@@state Language :desc Language (ISO 639-1) for generated text :value en @@@
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# Klammertext distribution test suite (subset).
|
# Klammertext distribution test suite (subset).
|
||||||
#
|
#
|
||||||
# Runs the five shell regression suites:
|
# Runs the six shell regression suites:
|
||||||
# cond_test.sh — @cond argument delimitation
|
# cond_test.sh — @cond argument delimitation
|
||||||
# deftype_test.sh — the four klammer definition modes + redefinition table
|
# deftype_test.sh — the four klammer definition modes + redefinition table
|
||||||
# escape_test.sh — target character escaping and quoted specials
|
# escape_test.sh — target character escaping and quoted specials
|
||||||
# filename_test.sh — filenames with spaces (quoting, " / " lists, rescue)
|
# filename_test.sh — filenames with spaces (quoting, " / " lists, rescue)
|
||||||
|
# modulepath_test.sh — @eval finds modules beside the file that names them
|
||||||
# editor_test.sh — editor support (doc/edit): indentation and table
|
# editor_test.sh — editor support (doc/edit): indentation and table
|
||||||
# alignment; needs python3, uses Emacs when installed
|
# alignment; needs python3, uses Emacs when installed
|
||||||
#
|
#
|
||||||
@@ -16,4 +17,5 @@ test:
|
|||||||
./deftype_test.sh
|
./deftype_test.sh
|
||||||
./escape_test.sh
|
./escape_test.sh
|
||||||
./filename_test.sh
|
./filename_test.sh
|
||||||
|
./modulepath_test.sh
|
||||||
./editor_test.sh
|
./editor_test.sh
|
||||||
|
|||||||
120
tst/modulepath_test.sh
Executable file
120
tst/modulepath_test.sh
Executable file
@@ -0,0 +1,120 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# modulepath_test.sh — @eval pathname resolution (Klammermachine engine
|
||||||
|
# tier): module resolution and the :cwd option.
|
||||||
|
#
|
||||||
|
# A Python module lives NEXT TO THE FILE whose @eval names it: the directory
|
||||||
|
# of every file the Machine reads — input files, klammer sets, @read targets
|
||||||
|
# — joins the Python sys.path (after the SKS directories), so resolution is
|
||||||
|
# independent of the cwd. Before 2026-07-27 modules were found only via
|
||||||
|
# PYTHONPATH or the cwd, so `ktext /path/doc.kt` worked from the document's
|
||||||
|
# directory and failed from anywhere else.
|
||||||
|
#
|
||||||
|
# The same directories are searched for a relative `:cpp` library not found
|
||||||
|
# from the cwd (Eval::eval_command); that path is not exercised here because
|
||||||
|
# it would mean compiling a DSO inside the test — the directory list is the
|
||||||
|
# same one these cases prove.
|
||||||
|
#
|
||||||
|
# Pass-ordering caveat (deliberate, untested here): at one nesting level
|
||||||
|
# @eval is processed before @read expands, so a TOP-LEVEL @eval sees only
|
||||||
|
# files read earlier. A klammer DEFINED in a read file is applied later and
|
||||||
|
# always sees its own directory — the realistic case, tested below.
|
||||||
|
#
|
||||||
|
# Engine tier: -k none, fixtures created inline in a temp tree, no SKS.
|
||||||
|
# Usage: ./modulepath_test.sh (ktext on PATH)
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
red=$'\033[31m'
|
||||||
|
green=$'\033[32m'
|
||||||
|
bold=$'\033[1m'
|
||||||
|
reset=$'\033[0m'
|
||||||
|
|
||||||
|
T="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$T"' EXIT
|
||||||
|
|
||||||
|
mkdir -p "$T/seta" "$T/setb" "$T/elsewhere"
|
||||||
|
printf 'def hello():\n return "module-next-to-read-file"\n' > "$T/seta/pmod.py"
|
||||||
|
printf '@@desc : @eval pmod.hello() @ @@#-\n' > "$T/seta/defs.k"
|
||||||
|
printf 'def here():\n return "module-next-to-input"\n' > "$T/setb/imod.py"
|
||||||
|
printf '@read %s/seta/defs.k @#-@desc@ / @eval imod.here() @\n' "$T" > "$T/setb/doc.kt"
|
||||||
|
printf '@eval nosuch_module_xyz.f() @\n' > "$T/setb/missing.kt"
|
||||||
|
|
||||||
|
check() { # check NAME EXPECTED ARGS...
|
||||||
|
local name="$1" expected="$2"; shift 2
|
||||||
|
local out
|
||||||
|
out=$(ktext "$@" -k none -d 2>/dev/null)
|
||||||
|
if [ "$out" = "$expected" ]; then
|
||||||
|
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1))
|
||||||
|
else
|
||||||
|
echo "${red}FAIL${reset} $name"
|
||||||
|
echo " expected: [$expected]"
|
||||||
|
echo " got: [$out]"; FAIL=$((FAIL + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "${bold}@eval module resolution tests${reset}"
|
||||||
|
echo "======================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
cd "$T/elsewhere"
|
||||||
|
|
||||||
|
check " 1. modules beside the @read file and the input file (absolute path)" \
|
||||||
|
"module-next-to-read-file / module-next-to-input" \
|
||||||
|
"$T/setb/doc.kt"
|
||||||
|
|
||||||
|
check " 2. the same, input given as a relative path" \
|
||||||
|
"module-next-to-read-file / module-next-to-input" \
|
||||||
|
"../setb/doc.kt"
|
||||||
|
|
||||||
|
name=" 3. an unknown module still reports cleanly"
|
||||||
|
err=$(ktext "$T/setb/missing.kt" -k none -d 2>&1 >/dev/null)
|
||||||
|
case "$err" in
|
||||||
|
*'Cannot import module "nosuch_module_xyz"'*)
|
||||||
|
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
||||||
|
*)
|
||||||
|
echo "${red}FAIL${reset} $name"
|
||||||
|
echo " stderr: [$(echo "$err" | head -3)]"; FAIL=$((FAIL + 1)) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- the :cwd option --------------------------------------------------------
|
||||||
|
# @eval :cwd DIR runs the eval (any mode) with DIR as the working directory
|
||||||
|
# and restores the process cwd afterwards; *K_input_dir* names the
|
||||||
|
# document's directory; whitespace-separated tokens are joined until they
|
||||||
|
# name an existing directory (the filenames-with-spaces convention).
|
||||||
|
|
||||||
|
printf 'hello from data file\n' > "$T/setb/data.txt"
|
||||||
|
printf '@eval :cwd *K_input_dir* :shell cat data.txt @\n' > "$T/setb/shellcwd.kt"
|
||||||
|
check " 4. :cwd *K_input_dir* for :shell, run from elsewhere" \
|
||||||
|
"hello from data file" "$T/setb/shellcwd.kt"
|
||||||
|
|
||||||
|
printf '@eval :cwd *K_input_dir* os.getcwd() @\n' > "$T/setb/pycwd.kt"
|
||||||
|
check " 5. :cwd changes the Python working directory" \
|
||||||
|
"$T/setb" "$T/setb/pycwd.kt"
|
||||||
|
|
||||||
|
printf '@eval :cwd *K_input_dir* :shell true @ / @eval :shell pwd @\n' > "$T/setb/restore.kt"
|
||||||
|
check " 6. the cwd is restored after the eval" \
|
||||||
|
"/ $T/elsewhere" "$T/setb/restore.kt"
|
||||||
|
|
||||||
|
mkdir -p "$T/sp ace"
|
||||||
|
printf 'spaced\n' > "$T/sp ace/f.txt"
|
||||||
|
printf '@eval :cwd %s/sp ace :shell cat f.txt @\n' "$T" > "$T/setb/spaces.kt"
|
||||||
|
check " 7. a :cwd directory containing spaces" \
|
||||||
|
"spaced" "$T/setb/spaces.kt"
|
||||||
|
|
||||||
|
name=" 8. a nonexistent :cwd is a clean argument error"
|
||||||
|
printf '@eval :cwd /nonexistent_kt_dir :shell true @\n' > "$T/setb/bad.kt"
|
||||||
|
err=$(ktext "$T/setb/bad.kt" -k none -d 2>&1 >/dev/null)
|
||||||
|
case "$err" in
|
||||||
|
*'The :cwd directory does not exist'*)
|
||||||
|
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
||||||
|
*)
|
||||||
|
echo "${red}FAIL${reset} $name"
|
||||||
|
echo " stderr: [$(echo "$err" | head -2)]"; FAIL=$((FAIL + 1)) ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "======================="
|
||||||
|
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
||||||
|
[ $FAIL -eq 0 ]
|
||||||
Reference in New Issue
Block a user