texlive[68919] Master: latex2pydata (20nov23)

commits+karl at tug.org commits+karl at tug.org
Mon Nov 20 21:51:05 CET 2023


Revision: 68919
          https://tug.org/svn/texlive?view=revision&revision=68919
Author:   karl
Date:     2023-11-20 21:51:05 +0100 (Mon, 20 Nov 2023)
Log Message:
-----------
latex2pydata (20nov23)

Modified Paths:
--------------
    trunk/Master/tlpkg/bin/tlpkg-ctan-check
    trunk/Master/tlpkg/tlpsrc/collection-latexextra.tlpsrc

Added Paths:
-----------
    trunk/Master/texmf-dist/doc/latex/latex2pydata/
    trunk/Master/texmf-dist/doc/latex/latex2pydata/README
    trunk/Master/texmf-dist/doc/latex/latex2pydata/latex2pydata.pdf
    trunk/Master/texmf-dist/source/latex/latex2pydata/
    trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.dtx
    trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.ins
    trunk/Master/texmf-dist/tex/latex/latex2pydata/
    trunk/Master/texmf-dist/tex/latex/latex2pydata/latex2pydata.sty
    trunk/Master/tlpkg/tlpsrc/latex2pydata.tlpsrc

Added: trunk/Master/texmf-dist/doc/latex/latex2pydata/README
===================================================================
--- trunk/Master/texmf-dist/doc/latex/latex2pydata/README	                        (rev 0)
+++ trunk/Master/texmf-dist/doc/latex/latex2pydata/README	2023-11-20 20:51:05 UTC (rev 68919)
@@ -0,0 +1,29 @@
+latex2pydata - write data to file in Python literal format
+
+Author:  Geoffrey M. Poore
+License:  LPPL v1.3c or later
+Development:  https://github.com/gpoore/latex2pydata_tex
+
+
+latex2pydata is a LaTeX package for writing data to file using Python literal
+syntax (https://docs.python.org/3/reference/lexical_analysis.html#literals).
+The data may be loaded safely in Python using the ast.literal_eval() function
+(https://docs.python.org/3/library/ast.html#ast.literal_eval) or the
+latex2pydata Python package https://github.com/gpoore/latex2pydata_py).
+
+The top-level data structure can be configured as either a Python dict or a
+list of dicts.  Within dicts, all keys and values are written to file as
+Python string literals.  However, this does not limit the data types that can
+be passed from LaTeX to Python.  When data is loaded, the included schema
+functionality makes it possible to convert string values into other Python
+data types such as dicts, lists, sets, bools, and numbers.
+
+The data is suitable for direct loading in Python with ast.literal_eval().
+It is also possible to load data using the latex2pydata Python package
+(https://github.com/gpoore/latex2pydata_py).  This functions as a wrapper for
+ast.literal_eval().  The package requires all keys to match the regex
+"[A-Za-z_][0-9A-Za-z_]*".  Periods in keys are interpreted as key paths and
+indicate sub-dicts.  For example, the key path "main.sub" represents a key
+"main" in the main dict that maps to a sub-dict containing a key "sub".  The
+Python package supports the schema features provided by the LaTeX package, so
+that data types other than dicts of strings are possible.


Property changes on: trunk/Master/texmf-dist/doc/latex/latex2pydata/README
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/doc/latex/latex2pydata/latex2pydata.pdf
===================================================================
(Binary files differ)

Index: trunk/Master/texmf-dist/doc/latex/latex2pydata/latex2pydata.pdf
===================================================================
--- trunk/Master/texmf-dist/doc/latex/latex2pydata/latex2pydata.pdf	2023-11-20 20:49:38 UTC (rev 68918)
+++ trunk/Master/texmf-dist/doc/latex/latex2pydata/latex2pydata.pdf	2023-11-20 20:51:05 UTC (rev 68919)

Property changes on: trunk/Master/texmf-dist/doc/latex/latex2pydata/latex2pydata.pdf
___________________________________________________________________
Added: svn:mime-type
## -0,0 +1 ##
+application/pdf
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.dtx
===================================================================
--- trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.dtx	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.dtx	2023-11-20 20:51:05 UTC (rev 68919)
@@ -0,0 +1,1280 @@
+% \iffalse meta-comment
+%
+% Copyright (C) 2023 by Geoffrey M. Poore <gpoore at gmail.com>
+% ---------------------------------------------------------------------------
+% This work may be distributed and/or modified under the
+% conditions of the LaTeX Project Public License, either version 1.3c
+% of this license or (at your option) any later version.
+% The latest version of this license is in
+%   http://www.latex-project.org/lppl.txt
+% and version 1.3c or later is part of all distributions of LaTeX
+% version 2008/05/04 or later.
+%
+% This work has the LPPL maintenance status `maintained'.
+%
+% The Current Maintainer of this work is Geoffrey M. Poore.
+%
+% This work consists of the files latex2pydata.dtx and latex2pydata.ins
+% and the derived filebase latex2pydata.sty.
+%
+% \fi
+%
+% \iffalse
+%<*driver>
+\ProvidesFile{latex2pydata.dtx}
+%</driver>
+%<package>\NeedsTeXFormat{LaTeX2e}[1999/12/01]
+%<package>\ProvidesPackage{latex2pydata}
+%<*package>
+    [2023/11/19 v0.1 latex2pydata - write data to file in Python literal format]
+%</package>
+%
+%<*driver>
+\documentclass{ltxdoc}
+
+\makeatletter
+
+\usepackage[T1]{fontenc}
+\usepackage[utf8]{inputenc}
+\usepackage{lmodern}
+\usepackage{fourier}
+\usepackage{microtype}
+
+\usepackage[svgnames]{xcolor}
+\usepackage{upquote}
+% The typesetting for macrocode doesn't use \@noligs, which upquote modifies.
+% So apply the upquote fix to \verbatim at nolig@list as well, which is in macrocode.
+\begingroup
+\catcode`'=\active
+\catcode``=\active
+\g at addto@macro\verbatim at nolig@list{%
+  \let'\textquotesingle
+  \let`\textasciigrave
+  \ifx\encodingdefault\upquote at OTone
+  \ifx\ttdefault\upquote at cmtt
+    \def'{\char13 }%
+    \def`{\char18 }%
+  \fi\fi}
+\endgroup
+\usepackage{fvextra}
+\usepackage{latex2pydata}
+
+\usepackage{graphicx}
+
+\usepackage{amsmath, amssymb}
+
+\usepackage{environ}
+
+\usepackage{tcolorbox}
+\tcbuselibrary{listings}
+% strip leading percent symbols
+\def\tcbverbatimwrite#1{%
+  \@bsphack
+  \tcb at set@verbatim at finish%
+  \tcb at allocate@tcb at out%
+  \immediate\openout\tcb at out #1
+  \tcb at verbatim@begin at hook%
+  \let\do\@makeother\dospecials
+  \tcb at verbatim@change at percent\catcode`\^^M\active \catcode`\^^I=12
+  \def\verbatim at processline{%
+    \immediate\write\tcb at out
+      {\expandafter\@gobble\the\verbatim at line}}%
+  \verbatim at start}%
+
+% fix redefinition by tcolorbox
+\def\verbatim at processline{%
+  \expandafter\check at percent\the\verbatim at line\par}
+
+\usepackage{hyperref}
+\hypersetup{
+  pdftitle=The latex2pydata package: write key-value data to file in Python literal format,
+  pdfauthor=Geoffrey M. Poore,
+  pdfsubject={latex2pydata LaTeX package manual},
+  colorlinks=true,
+  allcolors=ForestGreen,
+}
+\usepackage{cleveref}
+
+\let\code\Verb
+\def\meta#1{\mbox{\ensuremath{\langle}\textit{\ttfamily#1}\ensuremath{\rangle}}}
+
+% A more robust \cmd
+\let\cmd\Verb
+
+% Create a short verbatim pipe that handles quotation marks properly
+\begingroup
+\catcode`\|=\active
+\gdef\pipe at active@verbatim{%
+  \begingroup
+  \let\do\@makeother\dospecials
+  \catcode`\|=\active
+  \catcode`\`=\active
+  \catcode`\'=\active
+  \catcode`\<=\active
+  \catcode`\>=\active
+  \catcode`\-=\active
+  \catcode`\,=\active
+  \catcode`\ =\active
+  \pipe at active@verbatim at i}
+\gdef\pipe at active@verbatim at i#1|{%
+  \endgroup
+  \begingroup
+  \def\FV at SV@pipe at active@verbatim{%
+    \FV at Gobble
+    \expandafter\FV at ProcessLine\expandafter{#1}}%
+  %\let\FV at BeginVBox\relax
+  %\let\FV at EndVBox\relax
+  %\def\FV at BProcessLine##1{\FancyVerbFormatLine{##1}}%
+  \BUseVerbatim{pipe at active@verbatim}%
+  \endgroup}
+\AtBeginDocument{\let|\pipe at active@verbatim}
+\endgroup
+
+\newcommand\pkg[1]{\textsf{\mbox{#1}}}
+
+\def\MacroFont{%
+  \fontencoding\encodingdefault%
+  \fontfamily\ttdefault%
+  \fontseries\mddefault%
+  \fontshape\updefault%
+  \small}
+
+\def\PrintMacroName#1{{\strut\MacroFont\color{DarkGreen}\footnotesize\string #1\ }}
+
+\def\PrintDescribeMacro#1{\strut\MacroFont\textcolor{DarkGreen}{\string #1\ }}
+\let\PrintDescribeEnv\PrintDescribeMacro
+%\let\PrintMacroName\PrintDescribeMacro
+\let\PrintEnvName\PrintDescribeEnv
+
+\def\theCodelineNo{\textcolor{DarkGreen}{\sffamily\scriptsize{\arabic{CodelineNo}}}}
+
+
+\def\DescMacro{%
+  \begingroup\makeatletter\DescMacro at i}
+\def\DescMacro at i#1{%
+  \endgroup
+  \DescMacro at ii#1\FV at Sentinel
+}
+\def\DescMacro at ii#1#2\FV at Sentinel{%
+  \@nameuse{SpecialMacroIndex}{#1}%
+  \vspace{1em}%
+  \begingroup
+  \ttfamily
+  \large
+  \setlength{\parindent}{0pt}%
+  \hspace{-3em}{\bfseries\color{DarkGreen}\detokenize{#1}}{\normalsize#2}%
+  \endgroup
+  \par\vspace{0.5em}\noindent\ignorespaces
+}
+\def\DescEnv#1{%
+  \@nameuse{SpecialEnvIndex}{#1}%
+  \vspace{1em}%
+  \begingroup
+  \ttfamily
+  \large
+  \setlength{\parindent}{0pt}%
+  \hspace{-3em}{\bfseries\color{DarkGreen}\detokenize{#1}}\space%
+  \endgroup
+  (\textit{env.})
+  \par\vspace{0.5em}\noindent\ignorespaces
+}
+
+\let\orig at footnote\footnote
+\renewcommand{\footnote}{%
+  \begingroup
+  \let\do\@makeother
+  \dospecials
+  \catcode`\{=1
+  \catcode`\}=2
+  \new at footnote}
+\newcommand{\new at footnote}[1]{%
+  \endgroup
+  \orig at footnote{\scantokens{#1}}}
+
+\def\printopt#1(#2) (#3){%
+  \vspace{0.1in}%
+  \leavevmode%
+  \marginpar{\raggedleft\texttt{\small\textcolor{DarkGreen}{#1}}\ }%
+  \kern-\parindent\textsf{(#2)}\hfill(default: \texttt{#3})\\}
+
+\newenvironment{optionlist}%
+ {%
+  ~\par\vspace{-14pt}%
+  \def\pipechar{|}
+  \let\|\pipechar
+  \newcommand*\optionlistnext{}%
+  \renewcommand*\item[1][]{%
+    \optionlistnext%
+    \renewcommand*\optionlistnext{\par}%
+    \printopt##1%
+    \ignorespaces}}
+ {%
+  \par}
+
+\newenvironment{example}
+  {\VerbatimEnvironment
+   \begin{VerbatimOut}[gobble=4]{example.out}}
+  {\end{VerbatimOut}%
+   \vspace{1ex}%
+   \setlength{\parindent}{0pt}%
+   \setlength{\fboxsep}{1em}%
+   \fcolorbox{DarkGreen}{white}{\begin{minipage}{0.5\linewidth}%
+     \VerbatimInput{example.out}%
+   \end{minipage}%
+   \hspace{0.025\linewidth}%
+   {\color{DarkGreen}\vrule}%
+   \hspace{0.025\linewidth}%
+   \begin{minipage}{0.4\linewidth}%
+     \input{example.out}%
+   \end{minipage}%
+   }\vspace{1ex}}
+
+\newenvironment{longexample}
+  {\VerbatimEnvironment
+   \begin{VerbatimOut}[gobble=4]{example.out}}
+  {\end{VerbatimOut}%
+   \vspace{1ex}%
+   \setlength{\parindent}{0pt}%
+   \setlength{\fboxsep}{1em}%
+   \fcolorbox{DarkGreen}{white}{\begin{minipage}{0.94\linewidth}%
+     \VerbatimInput{example.out}%
+     {\color{DarkGreen}\hrulefill}
+     \setlength{\fboxsep}{3pt}%
+     \input{example.out}%
+   \end{minipage}%
+   }\vspace{1ex}}
+
+\CustomVerbatimEnvironment{VerbatimVerbatim}{Verbatim}{}
+
+\edef\hashchar{\string#}
+
+\newcommand{\changestext}{}
+\NewEnviron{changelog}[2]{%
+    \g at addto@macro\changestext{\item[#1] (#2) \begin{itemize}}%
+    \expandafter\g at addto@macro\expandafter\changestext\expandafter{\BODY}%
+    \g at addto@macro\changestext{\end{itemize}}%
+}
+\newcommand{\PrintChangelog}{%
+    %\addcontentsline{toc}{section}{Changelog}
+    %\section*{Changelog}%
+    \section{Changelog}
+    \label{sec:changelog}
+    \begin{description}%
+    \changestext
+    \end{description}%
+}
+
+\begingroup
+\catcode`\#=12\relax
+\gdef\astliteval{\href{https://docs.python.org/3/library/ast.html#ast.literal_eval}{\code{ast.literal_eval()}}}
+\endgroup
+\def\pydatapy{\href{https://github.com/gpoore/latex2pydata_py}{\pkg{latex2pydata} Python package}}
+\def\fvextra{\href{https://github.com/gpoore/fvextra/}{\pkg{fvextra}}}
+\def\fancyvrb{\href{https://ctan.org/pkg/fancyvrb}{\pkg{fancyvrb}}}
+
+%\EnableCrossrefs
+%\CodelineIndex
+%\RecordChanges
+
+\makeatother
+\begin{document}
+  \DocInput{latex2pydata.dtx}
+  %\PrintChanges
+  %\PrintIndex
+\end{document}
+%</driver>
+% \fi
+%
+%
+% \begin{changelog}{v0.1}{2023-11-19}
+% \item Initial release.
+% \end{changelog}
+%
+%
+% \DoNotIndex{\newcommand,\newenvironment}
+% \DoNotIndex{\#,\$,\%,\&,\@,\\,\{,\},\^,\_,\~,\ }
+% \DoNotIndex{\@ne}
+% \DoNotIndex{\advance,\begingroup,\catcode,\closein}
+% \DoNotIndex{\closeout,\day,\def,\edef,\else,\empty,\endgroup}
+% \DoNotIndex{\begin,\end,\bgroup,\egroup}
+%
+% \providecommand*{\url}{\texttt}
+% \newcommand{\pydata}{\pkg{latex2pydata}}
+% \GetFileInfo{latex2pydata.dtx}
+%
+% \title{\vspace{-0.5in}The \pydata\ package}
+% \author{Geoffrey M.\ Poore \\ \href{mailto://gpoore@gmail.com}{\texttt{gpoore at gmail.com}} \\ \href{https://github.com/gpoore/latex2pydata_tex}{\Verb{github.com/gpoore/latex2pydata_tex}}}
+% \date{\fileversion~from \filedate}
+%
+% \maketitle
+%
+% \begin{abstract}
+% \noindent\pydata\ is a \LaTeX\ package for writing data to file using \href{https://docs.python.org/3/reference/lexical_analysis.html#literals}{Python literal syntax}.  The data may then be loaded safely in Python using the \astliteval\ function or the \pydatapy.
+%
+% \vspace{1.25in}
+% \noindent The original development of this package was funded by a \href{https://tug.org/tc/devfund/grants.html}{\TeX\ Development Fund grant} from the \href{https://tug.org/}{\TeX\ Users Group}.  \pydata\ is part of the 2023 grant for improvements to the \href{https://github.com/gpoore/minted/}{\pkg{minted}} package.
+% \end{abstract}
+%
+%
+% \pagebreak
+% \begingroup
+% \makeatletter
+% ^^A https://tex.stackexchange.com/a/45165/10742
+% \patchcmd{\@dottedtocline}
+%   {\rightskip\@tocrmarg}
+%   {\rightskip\@tocrmarg plus 4em \hyphenpenalty\@M}
+%   {}{}
+% \makeatother
+% \tableofcontents
+% \endgroup
+% \pagebreak
+%
+%
+% \section{Introduction}
+%
+% The \pydata\ package is designed for passing data from \LaTeX\ into Python.  It writes data to file using \href{https://docs.python.org/3/reference/lexical_analysis.html#literals}{Python literal syntax}.  The data may then be loaded safely in Python using the \astliteval\ function or the \pydatapy.
+%
+% The data that \pydata\ writes to file can take two forms.  The top-level data structure can be configured as a Python dict.  This is appropriate for representing a single \LaTeX\ command or environment.  The top-level data structure can also be configured as a list of dicts.  This is useful for representing a sequence of \LaTeX\ commands or environments.  In both cases, all keys and values within dicts are written to file as Python string literals.  Thus, the overall data is |dict[str, str]| or |list[dict[str, str]]|.  This does not limit the data types that can be passed from LaTeX to Python, however.  When data is loaded, the included schema functionality makes it possible to convert string values into other Python data types such as dicts, lists, sets, bools, and numbers.
+%
+% The data is suitable for direct loading in Python with \astliteval.  It is also possible to load data with the \pydatapy, which serves as a wrapper for \code{ast.literal_eval()}.  The Python package requires all keys to match the regex \code{[A-Za-z_][0-9A-Za-z_]*}.  Periods in keys are interpreted as key paths and indicate sub-dicts.  For example, the key path |main.sub| represents a key |main| in the main dict that maps to a sub-dict containing a key |sub|.  This makes it convenient to represent nested dicts.
+%
+% \pkg{latex2pydata} optionally supports writing metadata to file, including basic schema definitions for values.  When the \pydatapy\ loads data with a schema definition for a given value, the value is initially loaded as a string, which is the verbatim text sent from \LaTeX.  Then this string is evaluated with \code{ast.literal_eval()}.  An error is raised if this process does not result in an object with the data type specified in the schema.
+%
+%
+%
+% \section{Example}
+%
+%\begin{tcblisting}{}
+%\pydatasetfilename{\jobname.pydata}
+%\pydatawritedictopen
+%\pydatawritekeyvalue{key}{value with "quote" and \backslash\ ...}
+%\pydatawritedictclose
+%\pydataclosefilename{\jobname.pydata}
+%\VerbatimInput{\jobname.pydata}
+%\end{tcblisting}
+%
+%
+%
+% \section{Design considerations}
+%
+% \pydata\ is intended for use with Python.  Python literal syntax was chosen instead of \href{https://www.json.org/json-en.html}{JSON} or another data format because it provides simpler compatibility with \LaTeX.
+% \begin{itemize}
+% \item It must be possible to serialize the contents of a \LaTeX\ environment verbatim.  Python literal syntax supports multi-line string literals, so this is straightforward:  write an opening multi-line string delimiter to file, write the environment contents a line at a time (backslash-escaping any delimiter characters), and finally write a closing multi-line string delimiter.  Meanwhile, JSON requires that all literal newlines in strings be replaced with ``\code{\n}''.  The naive \LaTeX\ implementation of this would be to accumulate the entire environment contents verbatim within a single macro and then perform newline substitutions.  For long environment contents, this can lead to buffer memory errors (\LaTeX's |buf_size|).  It should be possible to avoid this, but only with more creative algorithms that bring additional complexity.
+% \item Python literal syntax only requires that the backslash plus the string delimiter be escaped within strings.  JSON has the additional requirement that command characters be escaped.
+% \end{itemize}
+%
+% \pkg{latex2pydata} is designed for use with Python and there are no plans to add additional data formats for use with other languages.  Choosing Python literal syntax does make \pkg{latex2pydata} less compatible with other programming languages than JSON or some other formats would be.  However, the only data structures used are |dict[str, str]| and |list[dict[str, str]]|.  It should be straightforward to implement a parser for this subset of Python literal syntax in other languages.
+%
+% Data structures are limited to |dict[str, str]| and |list[dict[str, str]]| because the objective is to minimize the potential for errors during serialization and deserialization.  These are simple enough data structures that basic checking for incomplete or malformed data is possible on the \LaTeX\ side during writing or buffering.  More complex data types, such as floating point numbers or deeply nested dicts, would be difficult to validate on the \LaTeX\ side, so invalid values would tend to result in parse errors during deserialization in Python.  The current approach still allows for a broad variety of data types via a schema, with the advantage that it can be easier to give useful error messages during schema validation than during deserialization parsing.
+%
+%
+%
+% \section{Usage}
+%
+% Load the package as usual:  |\usepackage{latex2pydata}|.  There are no package options.
+%
+%
+% \subsection[Errors]{\textcolor{red}{Errors}}
+%
+% Most \LaTeX\ packages handle errors based on the |-interaction| and |-halt-on-error| command-line options, plus |\interactionmode| and associated macros.  With the common |-interaction=nonstopmode|, \LaTeX\ will continue after most errors except some related to missing external files.
+%
+% \pydata\ is designed to force \LaTeX\ to exit immediately after any \pydata\ errors.  \pydata\ is designed for serializing data to file, typically so that an external program (restricted or unrestricted shell escape, or otherwise) can process the data and potentially generate output intended for \LaTeX.  Data that is known to be incomplete or malformed should not be passed to external programs, particularly via shell escape.
+%
+% When \pydata\ forces \LaTeX\ to exit immediately, there will typically be a message similar to  ``\Verb[breaklines]{! Emergency stop [...] cannot \read from terminal in nonstop modes}.''  This is due to the mechanism that \pydata\ uses to force \LaTeX\ to exit.  To debug, go back further up the log to find the \pydata\ error message that caused exiting.
+%
+%
+% \subsection{File handling}
+%
+% \DescMacro{\pydatasetfilehandle\marg{filehandle}}
+% Configure writing to file using an existing file handle created with |\newwrite|.  This allows manual management of the file handle.  For example:
+% \begin{Verbatim}[gobble=2]
+% \newwrite\testdata
+% \immediate\openout\testdata=\jobname.pydata\relax
+% \pydatasetfilehandle{\testdata}
+% ...
+% \pydatareleasefilehandle{\testdata}
+% \immediate\closeout\testdata
+% \end{Verbatim}
+%
+% To switch from one file handle to another, simply use |\pydatasetfilehandle| with the new file handle.  When the file handle is no longer in use, |\pydatareleasefilehandle| is recommended (but not required) to remove references to the file handle and perform basic checking for incomplete or malformed data written to file.
+%
+% \DescMacro{\pydatareleasefilehandle\marg{filehandle}}
+% When a file handle is no longer needed, remove references to it.  Also perform basic checking for incomplete or malformed data written to file.
+%
+% This should only be used once per file handle, after all data has been written.  It is not needed when switching from one file handle to another when both files remain open; in that case, only |\pydatasetfilehandle| is needed.
+%
+% \DescMacro{\pydatasetfilename\marg{filename}}
+% Configure a file for writing based on filename, opening the file if necessary.  For example:
+% \begin{Verbatim}[gobble=2]
+% \pydatasetfilename{\jobname.pydata}
+% \end{Verbatim}
+% This is not designed for manual management of the file handle.  The file does not have to be closed manually since this will happen automatically at the end of the document.  However, using |\pydataclosefilename|\marg{filename} is recommended since it closes the file immediately and also performs basic checking for incomplete or malformed data written to file.
+%
+% To switch from one file to another, simply use |\pydatasetfilename| with the new filename.  When the file is no longer in use, |\pydataclosefilename| is recommended.
+%
+% \DescMacro{\pydataclosefilename\marg{filename}}
+% Close a file previously opened with |\pydatasetfilename|.  Also perform basic checking for incomplete or malformed data written to file.
+%
+%
+% \subsection{Metadata}
+%
+% \pkg{latex2pydata} optionally supports writing metadata to file, including basic schema definitions for values.  When data is loaded with the \pydatapy, the schema is used to perform type conversion and type checking.  When a schema definition exists for a given value, the value is initially loaded as a string, and then (for non-string data types) it is evaluated with \astliteval.  An error is raised if this process does not result in an object with the data type specified in the schema.
+%
+% \DescMacro{\pydatasetschemamissing\marg{missing behavior}}
+% This determines how the schema is processed when the schema is missing definitions for one or more key-value pairs.  Options for \meta{missing behavior}:
+% \begin{itemize}
+% \item |error| (default):  If a schema is defined then a complete schema is required.  That is, a schema definition must exist for all key-value pairs or an error is raised.
+% \item |rawstr|:  The schema is enforced for all key-value pairs for which it is defined, and any other key-value pairs are kept with string values.  These string values are the raw verbatim text passed from \LaTeX.
+% \item |evalany|:  The schema is enforced for all key-value pairs for which it is defined, and any other key-value pairs have the value evaluated with \astliteval, with all value data types being permitted.  Because all values without a schema definition are evaluated, any string values without a schema definition must be quoted and escaped as strings on the \LaTeX\ side.
+% \end{itemize}
+%
+% \DescMacro{\pydatasetschemakeytype\marg{key}\marg{value type}}
+% Define a key's schema.  For example, |\pydatasetschemakeytype{key}{int}|.
+%
+% \meta{value type} should be a standard Python type annotation, such as |list[int]| or |dict[str, float]|.  See the \pydatapy\ documentation for information about value data types that are currently supported.
+%
+% \DescMacro{\pydataclearschema}
+% Delete the existing schema.  If the schema is not deleted, it can be reused across multiple output files.
+%
+% \DescMacro{\pydatawritemeta}
+% Write metadata, including schema, to a file previously configured with |\pydatasetfilename| or |\pydatasetfilehandle|.  Metadata must always be the first thing written to file, before any data.
+%
+% \DescMacro{\pydataclearmeta}
+% Clear all metadata.  This includes deleting the schema and resetting schema missing behavior to the default.
+%
+%
+%
+% \subsection{Writing list and dict delimiters}
+% The overall data structure, before any schema is applied by the \pydatapy, can be either |list[dict[str, str]]| or |dict[str, str]|.  This determines which data collection delimiters are needed.
+%
+% Delimiters are written to the file previously configured via |\pydatasetfilehandle| or |\pydatasetfilename|.
+%
+% \DescMacro{\pydatawritedictopen}
+% Write an opening dict delimiter |{| to file.
+%
+% \DescMacro{\pydatawritedictclose}
+% Write a closing dict delimiter |}| to file.
+%
+% \DescMacro{\pydatawritelistopen}
+% Write an opening list delimiter |[| to file.
+%
+% \DescMacro{\pydatawritelistclose}
+% Write a closing list delimiter |]| to file.
+%
+%
+% \subsection{Writing keys and values}
+% All keys must be single-line strings of text without a newline.  Both single-line and multi-line values are supported.  Keys and values are written to the file previously configured via |\pydatasetfilehandle| or |\pydatasetfilename|.
+%
+% The \pydata\ commands read keys and values verbatim.  When these commands are used inside other commands, they use macros from \fvextra\ to attempt to interpret their arguments as verbatim.  However, there are limitations in this case because the arguments are already tokenized:
+% \begin{itemize}
+% \item |#| and |%| cannot be used.
+% \item Curly braces are only allowed in pairs.
+% \item Multiple adjacent spaces will be collapsed into a single space.
+% \item Be careful with backslashes.  A backslash that is followed by one or more ASCII letters will cause a following space to be lost, if the space is not immediately followed by an ASCII letter.
+% \item A single |^| is fine, but |^^| will serve as an escape sequence for an ASCII command character.
+% \end{itemize}
+% When the \pydata\ commands are used inside other commands that pass their arguments to the \pydata\ commands, it will usually be best to avoid these limitations by defining the other commands to read their arguments verbatim.  Consider using the \href{https://ctan.org/pkg/xparse}{\pkg{xparse}} package.  It is also possible to use |\FVExtraReadVArg| from \fvextra; for an example, see the implementation of |\pydatawritekey|.
+%
+% Because the \pydata\ commands treat keys and values as verbatim, any desired macro expansion must be performed before passing the keys and values to the \pydata\ commands.
+%
+% \DescMacro{\pydatawritekey\marg{key}}
+% Write a key to file.
+%
+% \DescMacro{\pydatawritevalue\marg{value}}
+% Write a single-line value to file.
+%
+% \DescMacro{\pydatawritekeyvalue\marg{key}\marg{value}}
+% Write a key and a single-line value to file simultaneously.
+%
+% \DescEnv{pydatawritemlvalue}
+% Write a multi-line value to file.
+%
+% This environment uses \fvextra\ and \fancyvrb\ internally to capture the environment contents verbatim.  If a new environment is defined as a wrapper for |pydatawritemlvalue|, then |\VerbatimEnvironment| must be used at the beginning of the new environment definition.  This configures \fancyvrb\ to find the end of the new environment correctly.
+%
+%
+% \subsection{Buffer}
+%
+% Key-value data can be written to file once a dict is opened with |\pydatawritedictopen|. It is also possible to accumulate key-value data in a ``buffer.''  This is convenient when the data serves as input to an external program that generates cached content.  Buffered data can be hashed in memory without being written to file, so the existence of cached content can be checked efficiently.
+%
+% A buffer consists of a sequence of macros of the form |\|\meta{buffername}|line|\meta{n}, where each line of data corresponds to a macro and \meta{n} is an integer greater than or equal to one (one-based indexing).  The length of the buffer is stored in the counter \meta{buffername}|length|.  Buffers are limited to containing comma-separated key-value data, without any opening or closing dict delimiters |{}|.
+%
+%
+% \subsubsection{Creating and deleting buffers}
+%
+% \DescMacro{\pydatasetbuffername\marg{buffername}}
+% Initialize a new buffer if \meta{buffername} has not been used previously, and configure all buffer operations to use \meta{buffername}.
+%
+% \meta{buffername} is used as a base name for creating the buffer line macros of the form |\|\meta{buffername}|line|\meta{n} and the buffer length counter \meta{buffername}|length|.
+%
+% \DescMacro{\pydataclearbuffername\marg{buffername}}
+% Delete the specified buffer.  |\let| all line macros |\|\meta{buffername}|line|\meta{n} to an undefined macro, and set the length counter \meta{buffername}|length| to zero.
+%
+%
+% \subsubsection{Special buffer operations}
+%
+% \DescMacro{\pydatabuffermdfivesum}
+% Calculate the MD5 hash of the current buffer, using |\pdf at mdfivesum| from \href{https://ctan.org/pkg/pdftexcmds}{\pkg{pdftexcmds}}.  This is fully expandable.  For example:
+% \begin{Verbatim}[gobble=2]
+% \edef\hash{\pydatabuffermdfivesum}
+% \end{Verbatim}
+%
+% \DescMacro{\pydatawritebuffer}
+% Write the current buffer to the file previously configured via |\pydatasetfilename| or |\pydatasetfilehandle|.
+%
+% Writing the buffer does not modify the buffer in any way or delete it.  To delete the buffer after writing, use |\pydataclearbuffername|.
+%
+%
+% \subsubsection{Buffering keys and values}
+% All keys must be single-line strings of text without a newline.  Both single-line and multi-line values are supported.  Keys and values are appended to the buffer previously configured via |\pydatasetbuffername|.
+%
+% The \pydata\ commands read keys and values verbatim.  Like the commands for writing keys and values, the commands for buffering keys and values have limitations when used inside other commands.
+%
+% \DescMacro{\pydatabufferkey\marg{key}}
+% Append a key to the buffer.
+%
+% \DescMacro{\pydatabuffervalue\marg{value}}
+% Append a single-line value to the buffer.
+%
+% \DescMacro{\pydatabufferkeyvalue\marg{key}\marg{value}}
+% Append a key and a single-line value to the buffer simultaneously.
+%
+% \DescEnv{pydatabuffermlvalue}
+% Append a multi-line value to the buffer.
+%
+% This environment uses \fvextra\ and \fancyvrb\ internally to capture the environment contents verbatim.  If a new environment is defined as a wrapper for |pydatabuffermlvalue|, then |\VerbatimEnvironment| must be used at the beginning of the new environment definition.  This configures \fancyvrb\ to find the end of the new environment correctly.
+%
+%
+%
+%
+% \PrintChangelog
+%
+% \StopEventually{\PrintIndex}
+%
+% \section{Implementation}
+%
+% \iffalse
+%<*package>
+% \fi
+%
+%
+%
+% \subsection{Exception handling}
+% \begin{macro}{\pydata at error}
+% Shortcut for error message.  The |\batchmode\read -1 to \pydata at exitnow| forces an immediate exit with ``\Verb[breaklines]{! Emergency stop [...] cannot \read from terminal in nonstop modes}.''  Due to the potentially critical nature of written or buffered data, any errors in assembling the data should be treated as fatal.
+%    \begin{macrocode}
+\def\pydata at error#1{%
+  \PackageError{latex2pydata}{#1}{}%
+  \batchmode\read -1 to \pydata at exitnow}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydata at warning}
+% Shortcut for warning message.
+%    \begin{macrocode}
+\def\pydata at warning#1{%
+  \PackageWarning{latex2pydata}{#1}}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \subsection{Required packages}
+%    \begin{macrocode}
+\RequirePackage{etoolbox}
+\RequirePackage{fvextra}
+\IfPackageAtLeastTF{fvextra}{2023/11/19}%
+ {}{\pydata at error{package fvextra is outdated; upgrade to the latest version}}
+\RequirePackage{pdftexcmds}
+%    \end{macrocode}
+%
+%
+%
+% \subsection{Util}
+%
+% \begin{macro}{\pydata at empty}
+% Empty macro.
+%    \begin{macrocode}
+\def\pydata at empty{}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \subsection{State}
+%
+% Track state of writing data and of buffering data.
+%
+% \begin{macro}{pydata at canwrite}
+% Whether data can be written.  False if a file handle has not been set or if the top-level data structure has been closed.
+%    \begin{macrocode}
+\newbool{pydata at canwrite}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydata at hasmeta}
+% Whether metadata was written.  Metadata is a \code{dict[str, str | dict[str, str]]}.
+%    \begin{macrocode}
+\newbool{pydata at hasmeta}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydata at topexists}
+% Whether the top-level data structure has been configured.  The top-level data structure can be a list or a dict.  The overall data structure must be either |dict[str, str]| or |list[dict[str, str]]|.
+%    \begin{macrocode}
+\newbool{pydata at topexists}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydata at topislist}
+% Whether the top-level data structure is a list.
+%    \begin{macrocode}
+\newbool{pydata at topislist}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydata at indict}
+% Whether a dict has been opened.
+%    \begin{macrocode}
+\newbool{pydata at indict}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydata at haskey}
+% Whether a key has been written (waiting for a value).
+%    \begin{macrocode}
+\newbool{pydata at haskey}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydata at fhstartstate, \pydata at fhstopstate}
+% Start and stop state tracking for a file handle (|\newwrite|).  Each file handle has its own set of state bools of the form |pydata@|\meta{boolname}|@|\meta{fh}.  When a file handle is in use, the values of these bools are copied into the |pydata@|\meta{boolname} bools; when the file handle is no longer in use, |pydata@|\meta{boolname} values are copied back into |pydata@|\meta{boolname}|@|\meta{fh}.
+%    \begin{macrocode}
+\def\pydata at fhstartstate#1{%
+  \expandafter\pydata at fhstartstate@i\expandafter{\number#1}}
+\newbool{pydata at fhnewstate}
+\def\pydata at fhstartstate@i#1{%
+  \ifcsname ifpydata at canwrite@#1\endcsname
+    \boolfalse{pydata at fhnewstate}%
+  \else
+    \booltrue{pydata at fhnewstate}%
+  \fi
+  \def\do##1{%
+    \providebool{pydata@##1@#1}%
+    \ifbool{pydata@##1@#1}{\booltrue{pydata@##1}}{\boolfalse{pydata@##1}}}%
+  \docsvlist{canwrite, hasmeta, topexists, topislist, indict, haskey}%
+  \ifbool{pydata at fhnewstate}{\booltrue{pydata at canwrite}{}}{}}
+\def\pydata at fhstopstate#1{%
+  \expandafter\pydata at fhstopstate@i\expandafter{\number#1}}
+\def\pydata at fhstopstate@i#1{%
+  \ifcsname ifpydata at canwrite@#1\endcsname
+    \def\do##1{%
+      \ifbool{pydata@##1}{\booltrue{pydata@##1@#1}}{\boolfalse{pydata@##1@#1}}%
+      \boolfalse{pydata@##1}}%
+    \docsvlist{canwrite, hasmeta, topexists, topislist, indict, haskey}%
+  \fi}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydata at bufferhaskey}
+% Whether a key has been added to the buffer (waiting for a value).
+%
+% If multiple buffers are in use, all buffers use the same |pydata at bufferhaskey|.  Inconsistent state is avoided by requiring that |\pydatasetbuffername| can only be invoked when |pydata at bufferhaskey| is false.
+%    \begin{macrocode}
+\newbool{pydata at bufferhaskey}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \subsection{File handle}
+%
+% \begin{macro}{\pydata at filehandle}
+% File handle for writing data.
+%    \begin{macrocode}
+\let\pydata at filehandle\relax
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydata at checkfilehandle}
+% Check whether file handle has been set.
+%    \begin{macrocode}
+\def\pydata at checkfilehandle{%
+  \ifx\pydata at filehandle\relax
+    \pydata at error{Undefined file handle; use \string\pydatasetfilehandle}%
+  \fi}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatasetfilehandle, \pydatareleasefilehandle}
+% Set and release file handle.  Release isn't strictly required, but it is necessary for basic data checking on the \LaTeX\ side.
+%    \begin{macrocode}
+\def\pydatasetfilehandle#1{%
+  \ifx\pydata at filehandle\relax
+  \else
+    \pydata at fhstopstate{\pydata at filehandle}%
+  \fi
+  \let\pydata at filehandle#1\relax
+  \pydata at fhstartstate{#1}}
+\def\pydatareleasefilehandle#1{%
+  \ifx\pydata at filehandle\relax
+  \else
+    \ifx\pydata at filehandle#1\relax
+      \pydata at fhstopstate{#1}%
+      \let\pydata at filehandle\relax
+    \fi
+  \fi
+  \ifcsname ifpydata at canwrite@\number#1\endcsname
+    \ifbool{pydata at canwrite@\number#1}%
+     {\ifbool{pydata at haskey@\number#1}%
+       {\pydata at error{Incomplete data: key is waiting for value}}{}%
+      \ifbool{pydata at indict@\number#1}%
+       {\pydata at error{Incomplete data: dict is not closed}}{}%
+      \ifbool{pydata at topislist@\number#1}%
+       {\pydata at error{Incomplete data: list is not closed}}{}}%
+    {}%
+  \fi}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatasetfilename, \pydataclosefilename}
+% Shortcut for creating a |\newwrite| and then passing the file handle to |\pydatasetfilehandle|.  Automatically attempt to close the file handle (if it still exists) at the end of the document.  This isn't strictly required since \TeX\ will \href{https://tex.stackexchange.com/a/337291}{automatically close open writes}.  Invoking the close macro is necessary for basic data checking on the \LaTeX\ side.
+%    \begin{macrocode}
+\def\pydatasetfilename#1{%
+  \ifcsname pydata at fh@#1\endcsname
+  \else
+    \expandafter\newwrite\csname pydata at fh@#1\endcsname
+    \expandafter\immediate\expandafter\openout\csname pydata at fh@#1\endcsname=#1\relax
+    \AtEndDocument{\pydataclosefilename{#1}}%
+  \fi
+  \expandafter\pydatasetfilehandle\expandafter{\csname pydata at fh@#1\endcsname}}
+\def\pydataclosefilename#1{%
+  \ifcsname pydata at fh@#1\endcsname
+    \expandafter\pydatareleasefilehandle\expandafter{\csname pydata at fh@#1\endcsname}%
+    \expandafter\immediate\expandafter\closeout\csname pydata at fh@#1\endcsname
+    \expandafter\let\csname pydata at fh@#1\endcsname\pydata at undefined
+  \fi}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \subsection{Buffer}
+%
+% Key-value data can be written directly to file once a dict is opened.  It is also possible to accumulate key-value data in a ``buffer.''  This is convenient when the data serves as input to an external program that generates cached content.  Buffered data can be hashed in memory without being written to file, so the existence of cached content can be checked efficiently.
+%
+% The buffer consists of a sequence of macros of the form |\<buffer_name>line<n>|, where each line of data corresponds to a macro and |<n>| is an integer greater than or equal to one.  The length of the buffer is stored in the counter |<buffer_name>length|.  The buffer includes comma-separated key-value data, without any opening or closing dict delimiters |{}|.
+%
+% \begin{macro}{pydata at bufferindex}
+% Counter for looping through buffers.
+%    \begin{macrocode}
+\newcounter{pydata at bufferindex}
+\setcounter{pydata at bufferindex}{0}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatasetbuffername, \pydata at buffername, \pydata at bufferlinename, \pydata at bufferlengthname}
+% Set the buffer base name and create a corresponding length counter if it does not exist.
+%    \begin{macrocode}
+\def\pydatasetbuffername#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot change buffers when a buffered key is waiting for a value}}%
+   {}%
+  \def\pydata at buffername{#1}%
+  \def\pydata at bufferlinename{#1line}%
+  \def\pydata at bufferlengthname{#1length}%
+  \ifcsname c@\pydata at bufferlengthname\endcsname
+  \else
+    \expandafter\newcounter\expandafter{\pydata at bufferlengthname}%
+  \fi
+  \expandafter\setcounter\expandafter{\pydata at bufferlengthname}{0}}
+\pydatasetbuffername{pydata at defaultbuffer}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatawritebuffer}
+% Write existing buffer macros to file handle.
+%    \begin{macrocode}
+\def\pydatawritebuffer{%
+  \ifnum\expandafter\value\expandafter{\pydata at bufferlengthname}<1\relax
+    \pydata at error{Cannot write empty buffer}%
+  \fi
+  \ifbool{pydata at indict}{}{\pydata at error{Cannot write a buffer unless in a dict}}%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot write buffer when a buffered key is waiting for a value}}{}%
+  \setcounter{pydata at bufferindex}{1}%
+  \loop\unless\ifnum\value{pydata at bufferindex}>%
+      \expandafter\value\expandafter{\pydata at bufferlengthname}\relax
+    \immediate\write\pydata at filehandle{%
+      \csname\pydata at bufferlinename\arabic{pydata at bufferindex}\endcsname}%
+    \stepcounter{pydata at bufferindex}%
+  \repeat
+  \setcounter{pydata at bufferindex}{0}}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\pydataclearbuffername}
+% Delete the buffer:  |\let| all line macros to an undefined macro, and set length to zero.
+%    \begin{macrocode}
+\def\pydataclearbuffername#1{%
+  \def\pydata at clearbuffername{#1}%
+  \ifcsname c@#1length\endcsname
+  \else
+    \pydata at error{Buffer #1 does not exist}%
+  \fi
+  \setcounter{pydata at bufferindex}{1}%
+  \loop\unless\ifnum\value{pydata at bufferindex}>\value{#1length}\relax
+    \expandafter\let
+      \csname#1line\arabic{pydata at bufferindex}\endcsname\pydata at undefined
+    \stepcounter{pydata at bufferindex}%
+  \repeat
+  \setcounter{#1length}{0}%
+  \setcounter{pydata at bufferindex}{0}%
+  \ifx\pydata at clearbuffername\pydata at buffername
+    \boolfalse{pydata at bufferhaskey}%
+  \fi}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\pydatabuffermdfivesum}
+% Calculate buffer MD5.
+%    \begin{macrocode}
+\def\pydatabuffermdfivesum{%
+  \pdf at mdfivesum{%
+    \ifnum\expandafter\value\expandafter{\pydata at bufferlengthname}<1
+      \expandafter\@firstoftwo
+    \else
+      \expandafter\@secondoftwo
+    \fi
+    {}{\pydatabuffermdfivesum at i{1}}}}
+\def\pydatabuffermdfivesum at i#1{%
+  \csname\pydata at bufferlinename#1\endcsname^^J%
+  \ifnum\expandafter\value\expandafter{\pydata at bufferlengthname}=#1
+    \expandafter\@gobble
+  \else
+    \expandafter\@firstofone
+  \fi
+  {\expandafter\pydatabuffermdfivesum at i\expandafter{\the\numexpr#1+1 }}}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \subsection{String processing}
+%
+% Ensure correct catcode for double quotation mark, which will be used for delimiting all Python string literals.
+%    \begin{macrocode}
+\begingroup
+\catcode`\"=12\relax
+%    \end{macrocode}
+%
+%
+% \begin{macro}{\pydata at escstrtext}
+% Escape string text by replacing |\| with |\\| and |"| with |\"|.  Any text that requires expansion must be expanded prior to escaping.  The string text is processed with |\detokenize| to ensure catcodes and prepare it for writing.  This is redundant in cases where text has already been processed with |\FVExtraDetokenizeVArg|.
+%    \begin{macrocode}
+\begingroup
+\catcode`\!=0
+!catcode`!\=12
+!gdef!pydata at escstrtext#1{%
+  !expandafter!pydata at escstrtext@i!detokenize{#1}\!FV at Sentinel}
+!gdef!pydata at escstrtext@i#1\#2!FV at Sentinel{%
+  !if!relax!detokenize{#2}!relax
+    !expandafter!@firstoftwo
+  !else
+    !expandafter!@secondoftwo
+  !fi
+   {!pydata at escstrtext@ii#1"!FV at Sentinel}%
+   {!pydata at escstrtext@ii#1\\"!FV at Sentinel!pydata at escstrtext@i#2!FV at Sentinel}}
+!gdef!pydata at escstrtext@ii#1"#2!FV at Sentinel{%
+  !if!relax!detokenize{#2}!relax
+    !expandafter!@firstoftwo
+  !else
+    !expandafter!@secondoftwo
+  !fi
+   {#1}%
+   {#1\"!pydata at escstrtext@ii#2!FV at Sentinel}}
+!endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\pydata at quotestr}
+% Escape a string then quote it with |"|.
+%    \begin{macrocode}
+\gdef\pydata at quotestr#1{%
+  "\pydata at escstrtext{#1}"}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\pydata at mlstropen, \pydata at mlstrclose}
+% Multi-line string delimiters.  The opening delimiter has a trailing backslash to prevent the string from starting with a newline.
+%    \begin{macrocode}
+\begingroup
+\catcode`\!=0
+!catcode`!\=12
+!gdef!pydata at mlstropen{"""\}
+!gdef!pydata at mlstrclose{"""}
+!endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \noindent End |"| catcode.
+%    \begin{macrocode}
+\endgroup
+%    \end{macrocode}
+%
+%
+%
+% \subsection{Metadata}
+%
+% \begin{macro}{\pydata at schema}
+% Macro storing key-value schema data.
+%    \begin{macrocode}
+\def\pydata at schema{}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatasetschemamissing, \pydata at schemamissing}
+% Define behavior for missing key-value pairs in a schema.
+%    \begin{macrocode}
+\let\pydata at schemamissing@error\relax
+\let\pydata at schemamissing@rawstr\relax
+\let\pydata at schemamissing@evalany\relax
+\def\pydatasetschemamissing#1{%
+  \ifcsname pydata at schemamissing@\detokenize{#1}\endcsname
+  \else
+    \pydata at error{Invalid schema missing setting #1}%
+  \fi
+  \def\pydata at schemamissing{#1}}
+\pydatasetschemamissing{error}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatasetschemakeytype}
+% Define a key's schema.  For example, |\pydatasetschemakeytype{key}{int}|.
+%    \begin{macrocode}
+\begingroup
+\catcode`\:=12\relax
+\catcode`\,=12\relax
+\gdef\pydatasetschemakeytype#1#2{%
+  \ifbool{pydata at hasmeta}{\pydata at error{Must create schema before writing metadata}}{}%
+  \ifbool{pydata at topexists}{\pydata at error{Must create schema before writing data}}{}%
+  \expandafter\def\expandafter\pydata at schema\expandafter{%
+    \pydata at schema\pydata at quotestr{#1}: \pydata at quotestr{#2}, }}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydataclearschema}
+% Delete existing schema.  This isn't done automatically upon writing so that a schema can be defined and then reused.
+%    \begin{macrocode}
+\def\pydataclearschema{%
+  \def\pydata at schema{}}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\pydataclearmeta}
+% Delete existing metadata.  This isn't done automatically upon writing so that metadata can be defined and then reused.
+%    \begin{macrocode}
+\def\pydataclearmeta{%
+  \pydatasetschemamissing{error}%
+  \pydataclearschema}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatawritemeta}
+% Write metadata to file, including any schema.
+%    \begin{macrocode}
+\begingroup
+\catcode`\:=12\relax
+\catcode`\#=12\relax
+\catcode`\,=12\relax
+\gdef\pydatawritemeta{%
+  \ifbool{pydata at canwrite}%
+   {}{\pydata at error{Data was already written; cannot write metadata}}%
+  \ifbool{pydata at hasmeta}{\pydata at error{Already wrote metadata}}{}%
+  \ifbool{pydata at topexists}{\pydata at error{Must write metadata before writing data}}{}%
+  \edef\pydata at meta@exp{%
+    # latex2pydata metadata:
+    \@charlb
+    \pydata at quotestr{schema_missing}:
+    \expandafter\pydata at quotestr\expandafter{\pydata at schemamissing},
+    \pydata at quotestr{schema}:
+    \ifx\pydata at schema\pydata at empty
+      \expandafter\@firstoftwo
+    \else
+      \expandafter\@secondoftwo
+    \fi
+     {None}{\@charlb\pydata at schema\@charrb},
+    \@charrb}%
+  \immediate\write\pydata at filehandle{\pydata at meta@exp}%
+  \booltrue{pydata at hasmeta}}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \subsection{Collection delimiters}
+%
+% \begin{macro}{\pydatawritelistopen, \pydatawritelistclose}
+% Write list delimiters.  These are only used when the top-level data structure is a list:  |list[dict[str, str]]|.
+%    \begin{macrocode}
+\begingroup
+\catcode`\[=12\relax
+\catcode`\]=12\relax
+\gdef\pydatawritelistopen{%
+  \pydata at checkfilehandle
+  \ifbool{pydata at canwrite}%
+   {}{\pydata at error{Data structure is closed; cannot write delim}}%
+  \ifbool{pydata at topexists}%
+   {\pydata at error{Top-level data structure already exists}}{}%
+  \immediate\write\pydata at filehandle{[}%
+  \booltrue{pydata at topexists}%
+  \booltrue{pydata at topislist}}
+\gdef\pydatawritelistclose{%
+  \ifbool{pydata at topexists}%
+   {}{\pydata at error{No data structure is open; cannot write delim}}%
+  \ifbool{pydata at topislist}%
+   {}{\pydata at error{Top-level data structure is not a list}}%
+  \ifbool{pydata at haskey}%
+   {\pydata at error{Cannot close data structure when key is waiting for value}}{}%
+  \immediate\write\pydata at filehandle{]}%
+  \boolfalse{pydata at topexists}%
+  \boolfalse{pydata at topislist}%
+  \boolfalse{pydata at hasmeta}%
+  \boolfalse{pydata at canwrite}}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\pydatawritedictopen, \pydatawritedictclose}
+% Write dict delimiters.  These are not the top-level data structure for |list[dict[str, str]]| but are the top-level data structure for |dict[str, str]|.
+%    \begin{macrocode}
+\begingroup
+\catcode`\,=12\relax
+\gdef\pydatawritedictopen{%
+  \ifbool{pydata at topislist}%
+   {\ifbool{pydata at indict}{\pydata at error{Already in a dict; cannot nest}}{}%
+    \immediate\write\pydata at filehandle{\@charlb}%
+    \booltrue{pydata at indict}}%
+   {\pydata at checkfilehandle
+    \ifbool{pydata at canwrite}%
+     {}{\pydata at error{Data structure is closed; cannot write delim}}%
+    \ifbool{pydata at topexists}%
+     {\pydata at error{Top-level data structure already exists}}{}%
+    \immediate\write\pydata at filehandle{\@charlb}%
+    \booltrue{pydata at topexists}%
+    \booltrue{pydata at indict}}}
+\gdef\pydatawritedictclose{%
+  \ifbool{pydata at indict}{}{\pydata at error{No dict is open; cannot write delim}}%
+  \ifbool{pydata at haskey}%
+   {\pydata at error{Cannot close data structure when key is waiting for value}}{}%
+  \ifbool{pydata at topislist}%
+   {\immediate\write\pydata at filehandle{\@charrb,}%
+    \boolfalse{pydata at indict}}%
+   {\immediate\write\pydata at filehandle{\@charrb}%
+    \boolfalse{pydata at indict}%
+    \boolfalse{pydata at topexists}%
+    \boolfalse{pydata at hasmeta}%
+    \boolfalse{pydata at canwrite}}}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \subsection{Keys and values}
+%
+% \begin{macro}{\pydatawritekey, \pydatabufferkey}
+% Write key to file or append it to the buffer.
+%    \begin{macrocode}
+\begingroup
+\catcode`\:=12\relax
+\gdef\pydatawritekey{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritekey at i}}}
+\gdef\pydatawritekey at i#1{%
+  \ifbool{pydata at indict}{}{\pydata at error{Cannot write a key unless in a dict}}%
+  \ifbool{pydata at haskey}{\pydata at error{Cannot write a key when waiting for a value}}{}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at quotestr{#1}:%
+  }%
+  \booltrue{pydata at haskey}}
+\gdef\pydatabufferkey{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabufferkey at i}}}
+\gdef\pydatabufferkey at i#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot buffer a key when waiting for a value}}{}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at quotestr{#1}:%
+    }%
+  \booltrue{pydata at bufferhaskey}}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatawritevalue, \pydatabuffervalue}
+% Write a value to file or append it to the buffer.
+%    \begin{macrocode}
+\begingroup
+\catcode`\,=12\relax
+\gdef\pydatawritevalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritevalue at i}}}
+\gdef\pydatawritevalue at i#1{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at quotestr{#1},%
+  }%
+  \boolfalse{pydata at haskey}}
+\gdef\pydatabuffervalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabuffervalue at i}}}
+\gdef\pydatabuffervalue at i#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at quotestr{#1},%
+    }%
+  \boolfalse{pydata at bufferhaskey}}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{\pydatawritekeyvalue, \pydatabufferkeyvalue}
+% Write a key and a single-line value to file simultaneously, or append them to the buffer.
+%    \begin{macrocode}
+\begingroup
+\catcode`\:=12\relax
+\catcode`\,=12\relax
+\gdef\pydatawritekeyvalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritekeyvalue at i}}}
+\gdef\pydatawritekeyvalue at i#1{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritekeyvalue at ii{#1}}}}
+\gdef\pydatawritekeyvalue at ii#1#2{%
+  \ifbool{pydata at indict}{}{\pydata at error{Cannot write a key unless in a dict}}%
+  \ifbool{pydata at haskey}{\pydata at error{Cannot write a key when waiting for a value}}{}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at quotestr{#1}: \pydata at quotestr{#2},%
+  }}
+\gdef\pydatabufferkeyvalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabufferkeyvalue at i}}}
+\gdef\pydatabufferkeyvalue at i#1{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabufferkeyvalue at ii{#1}}}}
+\gdef\pydatabufferkeyvalue at ii#1#2{%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot buffer a key when waiting for a value}}{}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at quotestr{#1}: \pydata at quotestr{#2},%
+    }}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+%
+% \begin{macro}{\pydatawritemlvaluestart, \pydatawritemlvalueline, \pydatawritemlvalueend, \pydatabuffermlvaluestart, \pydatabuffermlvalueline, \pydatabuffermlvalueend}
+%  Write a line of a multi-line value to file or append it to the buffer.  Write the end delimiter of the value to file or append it to the buffer.
+%    \begin{macrocode}
+\begingroup
+\catcode`\,=12\relax
+\gdef\pydatawritemlvaluestart{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at mlstropen
+  }}
+\gdef\pydatawritemlvalueline#1{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at escstrtext{#1}%
+  }}
+\gdef\pydatawritemlvalueend{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at mlstrclose,%
+  }%
+  \boolfalse{pydata at haskey}}
+\gdef\pydatabuffermlvaluestart{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at mlstropen
+    }}
+\gdef\pydatabuffermlvalueline#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at escstrtext{#1}%
+    }}
+\gdef\pydatabuffermlvalueend{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at mlstrclose,%
+    }%
+  \boolfalse{pydata at bufferhaskey}}
+\endgroup
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydatawritemlvalue}
+%    \begin{macrocode}
+\newenvironment{pydatawritemlvalue}%
+ {\VerbatimEnvironment
+  \pydatawritemlvaluestart
+  \begin{VerbatimWrite}[writer=\pydatawritemlvalueline]}%
+ {\end{VerbatimWrite}}
+\AfterEndEnvironment{pydatawritemlvalue}{\pydatawritemlvalueend}
+%    \end{macrocode}
+% \end{macro}
+%
+% \begin{macro}{pydatabuffermlvalue}
+%    \begin{macrocode}
+\newenvironment{pydatabuffermlvalue}%
+ {\VerbatimEnvironment
+  \begin{VerbatimBuffer}[buffername=pydata at tmpbuffer, globalbuffer=true]}%
+ {\end{VerbatimBuffer}}
+\AfterEndEnvironment{pydatabuffermlvalue}{%
+  \pydatabuffermlvaluestart
+  \setcounter{pydata at bufferindex}{1}%
+  \loop\unless\ifnum\value{pydata at bufferindex}>\value{pydata at tmpbufferlength}\relax
+    \expandafter\let\expandafter\pydata at tmpbufferline
+      \csname pydata at tmpbufferline\arabic{pydata at bufferindex}\endcsname
+    \expandafter\let
+      \csname pydata at tmpbufferline\arabic{pydata at bufferindex}\endcsname\pydata at undefined
+    \expandafter\pydatabuffermlvalueline\expandafter{\pydata at tmpbufferline}%
+    \stepcounter{pydata at bufferindex}%
+  \repeat
+  \setcounter{pydata at tmpbufferlength}{0}%
+  \setcounter{pydata at bufferindex}{0}%
+  \pydatabuffermlvalueend}
+%    \end{macrocode}
+% \end{macro}
+%
+%
+%
+% \iffalse
+%</package>
+% \fi
+%% \Finale
+\endinput


Property changes on: trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.dtx
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Added: trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.ins
===================================================================
--- trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.ins	                        (rev 0)
+++ trunk/Master/texmf-dist/source/latex/latex2pydata/latex2pydata.ins	2023-11-20 20:51:05 UTC (rev 68919)
@@ -0,0 +1,58 @@
+%% Copyright (C) 2023 by Geoffrey M. Poore <gpoore at gmail.com>
+%% --------------------------------------------------------------------------
+%% This work may be distributed and/or modified under the
+%% conditions of the LaTeX Project Public License, either version 1.3c
+%% of this license or (at your option) any later version.
+%% The latest version of this license is in
+%%   http://www.latex-project.org/lppl.txt
+%% and version 1.3c or later is part of all distributions of LaTeX
+%% version 2008/05/04 or later.
+%%
+%% This work has the LPPL maintenance status `maintained'.
+%%
+%% The Current Maintainer of this work is Geoffrey M. Poore.
+%%
+%% This work consists of the files latex2pydata.dtx and latex2pydata.ins
+%% and the derived filebase latex2pydata.sty.
+%%
+
+\input docstrip.tex
+\keepsilent
+\askforoverwritefalse
+
+\usedir{tex/latex/latex2pydata}
+
+\preamble
+
+This is a generated file.
+
+Copyright (C) 2023 by Geoffrey M. Poore <gpoore at gmail.com>
+--------------------------------------------------------------------------
+This work may be distributed and/or modified under the
+conditions of the LaTeX Project Public License, either version 1.3c
+of this license or (at your option) any later version.
+The latest version of this license is in
+  http://www.latex-project.org/lppl.txt
+and version 1.3c or later is part of all distributions of LaTeX
+version 2008/05/04 or later.
+
+\endpreamble
+
+\generate{\file{latex2pydata.sty}{\from{latex2pydata.dtx}{package}}}
+
+\obeyspaces
+\Msg{***************************************************************}
+\Msg{*                                                             *}
+\Msg{* To finish the installation you have to move the following   *}
+\Msg{* file into a directory searched by TeX:                      *}
+\Msg{*                                                             *}
+\Msg{*     latex2pydata.sty                                        *}
+\Msg{*                                                             *}
+\Msg{* To produce the documentation run the file latex2pydata.dtx  *}
+\Msg{* through LaTeX.                                              *}
+\Msg{*                                                             *}
+\Msg{* Happy TeXing!                                               *}
+\Msg{*                                                             *}
+\Msg{***************************************************************}
+
+\endbatchfile

Added: trunk/Master/texmf-dist/tex/latex/latex2pydata/latex2pydata.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/latex2pydata/latex2pydata.sty	                        (rev 0)
+++ trunk/Master/texmf-dist/tex/latex/latex2pydata/latex2pydata.sty	2023-11-20 20:51:05 UTC (rev 68919)
@@ -0,0 +1,456 @@
+%%
+%% This is file `latex2pydata.sty',
+%% generated with the docstrip utility.
+%%
+%% The original source files were:
+%%
+%% latex2pydata.dtx  (with options: `package')
+%% 
+%% This is a generated file.
+%% 
+%% Copyright (C) 2023 by Geoffrey M. Poore <gpoore at gmail.com>
+%% --------------------------------------------------------------------------
+%% This work may be distributed and/or modified under the
+%% conditions of the LaTeX Project Public License, either version 1.3c
+%% of this license or (at your option) any later version.
+%% The latest version of this license is in
+%%   http://www.latex-project.org/lppl.txt
+%% and version 1.3c or later is part of all distributions of LaTeX
+%% version 2008/05/04 or later.
+%% 
+\NeedsTeXFormat{LaTeX2e}[1999/12/01]
+\ProvidesPackage{latex2pydata}
+    [2023/11/19 v0.1 latex2pydata - write data to file in Python literal format]
+\def\pydata at error#1{%
+  \PackageError{latex2pydata}{#1}{}%
+  \batchmode\read -1 to \pydata at exitnow}
+\def\pydata at warning#1{%
+  \PackageWarning{latex2pydata}{#1}}
+\RequirePackage{etoolbox}
+\RequirePackage{fvextra}
+\IfPackageAtLeastTF{fvextra}{2023/11/19}%
+ {}{\pydata at error{package fvextra is outdated; upgrade to the latest version}}
+\RequirePackage{pdftexcmds}
+\def\pydata at empty{}
+\newbool{pydata at canwrite}
+\newbool{pydata at hasmeta}
+\newbool{pydata at topexists}
+\newbool{pydata at topislist}
+\newbool{pydata at indict}
+\newbool{pydata at haskey}
+\def\pydata at fhstartstate#1{%
+  \expandafter\pydata at fhstartstate@i\expandafter{\number#1}}
+\newbool{pydata at fhnewstate}
+\def\pydata at fhstartstate@i#1{%
+  \ifcsname ifpydata at canwrite@#1\endcsname
+    \boolfalse{pydata at fhnewstate}%
+  \else
+    \booltrue{pydata at fhnewstate}%
+  \fi
+  \def\do##1{%
+    \providebool{pydata@##1@#1}%
+    \ifbool{pydata@##1@#1}{\booltrue{pydata@##1}}{\boolfalse{pydata@##1}}}%
+  \docsvlist{canwrite, hasmeta, topexists, topislist, indict, haskey}%
+  \ifbool{pydata at fhnewstate}{\booltrue{pydata at canwrite}{}}{}}
+\def\pydata at fhstopstate#1{%
+  \expandafter\pydata at fhstopstate@i\expandafter{\number#1}}
+\def\pydata at fhstopstate@i#1{%
+  \ifcsname ifpydata at canwrite@#1\endcsname
+    \def\do##1{%
+      \ifbool{pydata@##1}{\booltrue{pydata@##1@#1}}{\boolfalse{pydata@##1@#1}}%
+      \boolfalse{pydata@##1}}%
+    \docsvlist{canwrite, hasmeta, topexists, topislist, indict, haskey}%
+  \fi}
+\newbool{pydata at bufferhaskey}
+\let\pydata at filehandle\relax
+\def\pydata at checkfilehandle{%
+  \ifx\pydata at filehandle\relax
+    \pydata at error{Undefined file handle; use \string\pydatasetfilehandle}%
+  \fi}
+\def\pydatasetfilehandle#1{%
+  \ifx\pydata at filehandle\relax
+  \else
+    \pydata at fhstopstate{\pydata at filehandle}%
+  \fi
+  \let\pydata at filehandle#1\relax
+  \pydata at fhstartstate{#1}}
+\def\pydatareleasefilehandle#1{%
+  \ifx\pydata at filehandle\relax
+  \else
+    \ifx\pydata at filehandle#1\relax
+      \pydata at fhstopstate{#1}%
+      \let\pydata at filehandle\relax
+    \fi
+  \fi
+  \ifcsname ifpydata at canwrite@\number#1\endcsname
+    \ifbool{pydata at canwrite@\number#1}%
+     {\ifbool{pydata at haskey@\number#1}%
+       {\pydata at error{Incomplete data: key is waiting for value}}{}%
+      \ifbool{pydata at indict@\number#1}%
+       {\pydata at error{Incomplete data: dict is not closed}}{}%
+      \ifbool{pydata at topislist@\number#1}%
+       {\pydata at error{Incomplete data: list is not closed}}{}}%
+    {}%
+  \fi}
+\def\pydatasetfilename#1{%
+  \ifcsname pydata at fh@#1\endcsname
+  \else
+    \expandafter\newwrite\csname pydata at fh@#1\endcsname
+    \expandafter\immediate\expandafter\openout\csname pydata at fh@#1\endcsname=#1\relax
+    \AtEndDocument{\pydataclosefilename{#1}}%
+  \fi
+  \expandafter\pydatasetfilehandle\expandafter{\csname pydata at fh@#1\endcsname}}
+\def\pydataclosefilename#1{%
+  \ifcsname pydata at fh@#1\endcsname
+    \expandafter\pydatareleasefilehandle\expandafter{\csname pydata at fh@#1\endcsname}%
+    \expandafter\immediate\expandafter\closeout\csname pydata at fh@#1\endcsname
+    \expandafter\let\csname pydata at fh@#1\endcsname\pydata at undefined
+  \fi}
+\newcounter{pydata at bufferindex}
+\setcounter{pydata at bufferindex}{0}
+\def\pydatasetbuffername#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot change buffers when a buffered key is waiting for a value}}%
+   {}%
+  \def\pydata at buffername{#1}%
+  \def\pydata at bufferlinename{#1line}%
+  \def\pydata at bufferlengthname{#1length}%
+  \ifcsname c@\pydata at bufferlengthname\endcsname
+  \else
+    \expandafter\newcounter\expandafter{\pydata at bufferlengthname}%
+  \fi
+  \expandafter\setcounter\expandafter{\pydata at bufferlengthname}{0}}
+\pydatasetbuffername{pydata at defaultbuffer}
+\def\pydatawritebuffer{%
+  \ifnum\expandafter\value\expandafter{\pydata at bufferlengthname}<1\relax
+    \pydata at error{Cannot write empty buffer}%
+  \fi
+  \ifbool{pydata at indict}{}{\pydata at error{Cannot write a buffer unless in a dict}}%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot write buffer when a buffered key is waiting for a value}}{}%
+  \setcounter{pydata at bufferindex}{1}%
+  \loop\unless\ifnum\value{pydata at bufferindex}>%
+      \expandafter\value\expandafter{\pydata at bufferlengthname}\relax
+    \immediate\write\pydata at filehandle{%
+      \csname\pydata at bufferlinename\arabic{pydata at bufferindex}\endcsname}%
+    \stepcounter{pydata at bufferindex}%
+  \repeat
+  \setcounter{pydata at bufferindex}{0}}
+\def\pydataclearbuffername#1{%
+  \def\pydata at clearbuffername{#1}%
+  \ifcsname c@#1length\endcsname
+  \else
+    \pydata at error{Buffer #1 does not exist}%
+  \fi
+  \setcounter{pydata at bufferindex}{1}%
+  \loop\unless\ifnum\value{pydata at bufferindex}>\value{#1length}\relax
+    \expandafter\let
+      \csname#1line\arabic{pydata at bufferindex}\endcsname\pydata at undefined
+    \stepcounter{pydata at bufferindex}%
+  \repeat
+  \setcounter{#1length}{0}%
+  \setcounter{pydata at bufferindex}{0}%
+  \ifx\pydata at clearbuffername\pydata at buffername
+    \boolfalse{pydata at bufferhaskey}%
+  \fi}
+\def\pydatabuffermdfivesum{%
+  \pdf at mdfivesum{%
+    \ifnum\expandafter\value\expandafter{\pydata at bufferlengthname}<1
+      \expandafter\@firstoftwo
+    \else
+      \expandafter\@secondoftwo
+    \fi
+    {}{\pydatabuffermdfivesum at i{1}}}}
+\def\pydatabuffermdfivesum at i#1{%
+  \csname\pydata at bufferlinename#1\endcsname^^J%
+  \ifnum\expandafter\value\expandafter{\pydata at bufferlengthname}=#1
+    \expandafter\@gobble
+  \else
+    \expandafter\@firstofone
+  \fi
+  {\expandafter\pydatabuffermdfivesum at i\expandafter{\the\numexpr#1+1 }}}
+\begingroup
+\catcode`\"=12\relax
+\begingroup
+\catcode`\!=0
+!catcode`!\=12
+!gdef!pydata at escstrtext#1{%
+  !expandafter!pydata at escstrtext@i!detokenize{#1}\!FV at Sentinel}
+!gdef!pydata at escstrtext@i#1\#2!FV at Sentinel{%
+  !if!relax!detokenize{#2}!relax
+    !expandafter!@firstoftwo
+  !else
+    !expandafter!@secondoftwo
+  !fi
+   {!pydata at escstrtext@ii#1"!FV at Sentinel}%
+   {!pydata at escstrtext@ii#1\\"!FV at Sentinel!pydata at escstrtext@i#2!FV at Sentinel}}
+!gdef!pydata at escstrtext@ii#1"#2!FV at Sentinel{%
+  !if!relax!detokenize{#2}!relax
+    !expandafter!@firstoftwo
+  !else
+    !expandafter!@secondoftwo
+  !fi
+   {#1}%
+   {#1\"!pydata at escstrtext@ii#2!FV at Sentinel}}
+!endgroup
+\gdef\pydata at quotestr#1{%
+  "\pydata at escstrtext{#1}"}
+\begingroup
+\catcode`\!=0
+!catcode`!\=12
+!gdef!pydata at mlstropen{"""\}
+!gdef!pydata at mlstrclose{"""}
+!endgroup
+\endgroup
+\def\pydata at schema{}
+\let\pydata at schemamissing@error\relax
+\let\pydata at schemamissing@rawstr\relax
+\let\pydata at schemamissing@evalany\relax
+\def\pydatasetschemamissing#1{%
+  \ifcsname pydata at schemamissing@\detokenize{#1}\endcsname
+  \else
+    \pydata at error{Invalid schema missing setting #1}%
+  \fi
+  \def\pydata at schemamissing{#1}}
+\pydatasetschemamissing{error}
+\begingroup
+\catcode`\:=12\relax
+\catcode`\,=12\relax
+\gdef\pydatasetschemakeytype#1#2{%
+  \ifbool{pydata at hasmeta}{\pydata at error{Must create schema before writing metadata}}{}%
+  \ifbool{pydata at topexists}{\pydata at error{Must create schema before writing data}}{}%
+  \expandafter\def\expandafter\pydata at schema\expandafter{%
+    \pydata at schema\pydata at quotestr{#1}: \pydata at quotestr{#2}, }}
+\endgroup
+\def\pydataclearschema{%
+  \def\pydata at schema{}}
+\def\pydataclearmeta{%
+  \pydatasetschemamissing{error}%
+  \pydataclearschema}
+\begingroup
+\catcode`\:=12\relax
+\catcode`\#=12\relax
+\catcode`\,=12\relax
+\gdef\pydatawritemeta{%
+  \ifbool{pydata at canwrite}%
+   {}{\pydata at error{Data was already written; cannot write metadata}}%
+  \ifbool{pydata at hasmeta}{\pydata at error{Already wrote metadata}}{}%
+  \ifbool{pydata at topexists}{\pydata at error{Must write metadata before writing data}}{}%
+  \edef\pydata at meta@exp{%
+    # latex2pydata metadata:
+    \@charlb
+    \pydata at quotestr{schema_missing}:
+    \expandafter\pydata at quotestr\expandafter{\pydata at schemamissing},
+    \pydata at quotestr{schema}:
+    \ifx\pydata at schema\pydata at empty
+      \expandafter\@firstoftwo
+    \else
+      \expandafter\@secondoftwo
+    \fi
+     {None}{\@charlb\pydata at schema\@charrb},
+    \@charrb}%
+  \immediate\write\pydata at filehandle{\pydata at meta@exp}%
+  \booltrue{pydata at hasmeta}}
+\endgroup
+\begingroup
+\catcode`\[=12\relax
+\catcode`\]=12\relax
+\gdef\pydatawritelistopen{%
+  \pydata at checkfilehandle
+  \ifbool{pydata at canwrite}%
+   {}{\pydata at error{Data structure is closed; cannot write delim}}%
+  \ifbool{pydata at topexists}%
+   {\pydata at error{Top-level data structure already exists}}{}%
+  \immediate\write\pydata at filehandle{[}%
+  \booltrue{pydata at topexists}%
+  \booltrue{pydata at topislist}}
+\gdef\pydatawritelistclose{%
+  \ifbool{pydata at topexists}%
+   {}{\pydata at error{No data structure is open; cannot write delim}}%
+  \ifbool{pydata at topislist}%
+   {}{\pydata at error{Top-level data structure is not a list}}%
+  \ifbool{pydata at haskey}%
+   {\pydata at error{Cannot close data structure when key is waiting for value}}{}%
+  \immediate\write\pydata at filehandle{]}%
+  \boolfalse{pydata at topexists}%
+  \boolfalse{pydata at topislist}%
+  \boolfalse{pydata at hasmeta}%
+  \boolfalse{pydata at canwrite}}
+\endgroup
+\begingroup
+\catcode`\,=12\relax
+\gdef\pydatawritedictopen{%
+  \ifbool{pydata at topislist}%
+   {\ifbool{pydata at indict}{\pydata at error{Already in a dict; cannot nest}}{}%
+    \immediate\write\pydata at filehandle{\@charlb}%
+    \booltrue{pydata at indict}}%
+   {\pydata at checkfilehandle
+    \ifbool{pydata at canwrite}%
+     {}{\pydata at error{Data structure is closed; cannot write delim}}%
+    \ifbool{pydata at topexists}%
+     {\pydata at error{Top-level data structure already exists}}{}%
+    \immediate\write\pydata at filehandle{\@charlb}%
+    \booltrue{pydata at topexists}%
+    \booltrue{pydata at indict}}}
+\gdef\pydatawritedictclose{%
+  \ifbool{pydata at indict}{}{\pydata at error{No dict is open; cannot write delim}}%
+  \ifbool{pydata at haskey}%
+   {\pydata at error{Cannot close data structure when key is waiting for value}}{}%
+  \ifbool{pydata at topislist}%
+   {\immediate\write\pydata at filehandle{\@charrb,}%
+    \boolfalse{pydata at indict}}%
+   {\immediate\write\pydata at filehandle{\@charrb}%
+    \boolfalse{pydata at indict}%
+    \boolfalse{pydata at topexists}%
+    \boolfalse{pydata at hasmeta}%
+    \boolfalse{pydata at canwrite}}}
+\endgroup
+\begingroup
+\catcode`\:=12\relax
+\gdef\pydatawritekey{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritekey at i}}}
+\gdef\pydatawritekey at i#1{%
+  \ifbool{pydata at indict}{}{\pydata at error{Cannot write a key unless in a dict}}%
+  \ifbool{pydata at haskey}{\pydata at error{Cannot write a key when waiting for a value}}{}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at quotestr{#1}:%
+  }%
+  \booltrue{pydata at haskey}}
+\gdef\pydatabufferkey{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabufferkey at i}}}
+\gdef\pydatabufferkey at i#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot buffer a key when waiting for a value}}{}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at quotestr{#1}:%
+    }%
+  \booltrue{pydata at bufferhaskey}}
+\endgroup
+\begingroup
+\catcode`\,=12\relax
+\gdef\pydatawritevalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritevalue at i}}}
+\gdef\pydatawritevalue at i#1{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at quotestr{#1},%
+  }%
+  \boolfalse{pydata at haskey}}
+\gdef\pydatabuffervalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabuffervalue at i}}}
+\gdef\pydatabuffervalue at i#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at quotestr{#1},%
+    }%
+  \boolfalse{pydata at bufferhaskey}}
+\endgroup
+\begingroup
+\catcode`\:=12\relax
+\catcode`\,=12\relax
+\gdef\pydatawritekeyvalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritekeyvalue at i}}}
+\gdef\pydatawritekeyvalue at i#1{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatawritekeyvalue at ii{#1}}}}
+\gdef\pydatawritekeyvalue at ii#1#2{%
+  \ifbool{pydata at indict}{}{\pydata at error{Cannot write a key unless in a dict}}%
+  \ifbool{pydata at haskey}{\pydata at error{Cannot write a key when waiting for a value}}{}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at quotestr{#1}: \pydata at quotestr{#2},%
+  }}
+\gdef\pydatabufferkeyvalue{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabufferkeyvalue at i}}}
+\gdef\pydatabufferkeyvalue at i#1{%
+  \FVExtraReadVArg{\FVExtraDetokenizeVArg{\pydatabufferkeyvalue at ii{#1}}}}
+\gdef\pydatabufferkeyvalue at ii#1#2{%
+  \ifbool{pydata at bufferhaskey}%
+   {\pydata at error{Cannot buffer a key when waiting for a value}}{}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at quotestr{#1}: \pydata at quotestr{#2},%
+    }}
+\endgroup
+\begingroup
+\catcode`\,=12\relax
+\gdef\pydatawritemlvaluestart{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at mlstropen
+  }}
+\gdef\pydatawritemlvalueline#1{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at escstrtext{#1}%
+  }}
+\gdef\pydatawritemlvalueend{%
+  \ifbool{pydata at haskey}{}{\pydata at error{Cannot write value when waiting for a key}}%
+  \immediate\write\pydata at filehandle{%
+    \pydata at mlstrclose,%
+  }%
+  \boolfalse{pydata at haskey}}
+\gdef\pydatabuffermlvaluestart{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at mlstropen
+    }}
+\gdef\pydatabuffermlvalueline#1{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at escstrtext{#1}%
+    }}
+\gdef\pydatabuffermlvalueend{%
+  \ifbool{pydata at bufferhaskey}%
+   {}{\pydata at error{Cannot buffer value when waiting for a key}}%
+  \expandafter\stepcounter\expandafter{\pydata at bufferlengthname}%
+  \expandafter\edef\csname
+    \pydata at bufferlinename\expandafter\arabic\expandafter{\pydata at bufferlengthname}%
+    \endcsname{%
+      \pydata at mlstrclose,%
+    }%
+  \boolfalse{pydata at bufferhaskey}}
+\endgroup
+\newenvironment{pydatawritemlvalue}%
+ {\VerbatimEnvironment
+  \pydatawritemlvaluestart
+  \begin{VerbatimWrite}[writer=\pydatawritemlvalueline]}%
+ {\end{VerbatimWrite}}
+\AfterEndEnvironment{pydatawritemlvalue}{\pydatawritemlvalueend}
+\newenvironment{pydatabuffermlvalue}%
+ {\VerbatimEnvironment
+  \begin{VerbatimBuffer}[buffername=pydata at tmpbuffer, globalbuffer=true]}%
+ {\end{VerbatimBuffer}}
+\AfterEndEnvironment{pydatabuffermlvalue}{%
+  \pydatabuffermlvaluestart
+  \setcounter{pydata at bufferindex}{1}%
+  \loop\unless\ifnum\value{pydata at bufferindex}>\value{pydata at tmpbufferlength}\relax
+    \expandafter\let\expandafter\pydata at tmpbufferline
+      \csname pydata at tmpbufferline\arabic{pydata at bufferindex}\endcsname
+    \expandafter\let
+      \csname pydata at tmpbufferline\arabic{pydata at bufferindex}\endcsname\pydata at undefined
+    \expandafter\pydatabuffermlvalueline\expandafter{\pydata at tmpbufferline}%
+    \stepcounter{pydata at bufferindex}%
+  \repeat
+  \setcounter{pydata at tmpbufferlength}{0}%
+  \setcounter{pydata at bufferindex}{0}%
+  \pydatabuffermlvalueend}
+%% \Finale
+\endinput
+%%
+%% End of file `latex2pydata.sty'.


Property changes on: trunk/Master/texmf-dist/tex/latex/latex2pydata/latex2pydata.sty
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/tlpkg/bin/tlpkg-ctan-check
===================================================================
--- trunk/Master/tlpkg/bin/tlpkg-ctan-check	2023-11-20 20:49:38 UTC (rev 68918)
+++ trunk/Master/tlpkg/bin/tlpkg-ctan-check	2023-11-20 20:51:05 UTC (rev 68919)
@@ -481,7 +481,7 @@
     latex-tools-dev latex-uni8
     latex-veryshortguide latex-via-exemplos latex-web-companion
     latex2e-help-texinfo latex2e-help-texinfo-fr
-    latex2e-help-texinfo-spanish latex2man latex2nemeth
+    latex2e-help-texinfo-spanish latex2man latex2nemeth latex2pydata
     latex4musicians latex4wp latex4wp-it latexbangla latexbug
     latexcheat latexcheat-de latexcheat-esmx latexcheat-ptbr
     latexcolors latexcourse-rug

Modified: trunk/Master/tlpkg/tlpsrc/collection-latexextra.tlpsrc
===================================================================
--- trunk/Master/tlpkg/tlpsrc/collection-latexextra.tlpsrc	2023-11-20 20:49:38 UTC (rev 68918)
+++ trunk/Master/tlpkg/tlpsrc/collection-latexextra.tlpsrc	2023-11-20 20:51:05 UTC (rev 68919)
@@ -773,6 +773,7 @@
 depend latex-lab-dev
 depend latex-tools-dev
 depend latex-uni8
+depend latex2pydata
 depend latexcolors
 depend latexdemo
 depend latexgit

Added: trunk/Master/tlpkg/tlpsrc/latex2pydata.tlpsrc
===================================================================


More information about the tex-live-commits mailing list.