Skip to content

Commit

Permalink
Fixed dependency pyyaml and tox
Browse files Browse the repository at this point in the history
  • Loading branch information
arthexis committed Mar 9, 2023
1 parent 1c359a6 commit 00d59d4
Show file tree
Hide file tree
Showing 18 changed files with 329 additions and 139 deletions.
7 changes: 0 additions & 7 deletions LICENSE

This file was deleted.

168 changes: 134 additions & 34 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ Nesting can be done to any depth. If quotes are avoided around the inner sigils,
data types will be preserved until the final interpolation.
If quotes are used, the result is always a string.

Sigils should *always* be uppercase. This is not enforced, but may cause
problems if you use lowercase sigils in your templates. Args are case-sensitive.


Whitespace
----------
Expand Down Expand Up @@ -247,8 +250,52 @@ except as specified in this document:
Quotes can be used interchangeably, but they must be balanced.


Four Tools are Available
------------------------
Available Tools
---------------

Sigils comes with a number of tools that can be used to manipulate and
interpolate sigils. You can load them individually, or all at once:

.. code-block:: python
from sigils import *
# or
from sigils import context, spool, splice, execute, vanish
Context
~~~~~~~

The *context* function is a context manager that can be used to manage
the context for the other functions. It can be used to set the context
for a single function call, or for a block of code.

.. code-block:: python
from sigils import context, execute
with context(
USERNAME="arthexis",
SETTING={"BASE_DIR": "/home/arth/webapp"},
):
result = execute("print('[[USERNAME]]')")
assert result == "arthexis"
You can also pass a filename to the *context* function, which will try to
guess the format and load the context from the file. The file can be in
JSON, YAML, TOML or INI format.

.. code-block:: python
from sigils import context, execute
with context("context.json"):
result = execute("print('[[USERNAME]]')")
assert result == "arthexis"
Spool
~~~~~

The *spool* function iterates over all sigils in a string, yielding each one
in the same order they appear in the string, without resolving them.
Expand All @@ -260,8 +307,7 @@ in the same order they appear in the string, without resolving them.
sql = "select * from users where username = [[USER]]"
assert list(spool(sql)) == ["[[USER]]"]
Spoolling is a fast way to check if a string contains sigils without hitting the ORM
Spool is a fast way to check if a string contains sigils without hitting the ORM
or the network. For example:

.. code-block:: python
Expand All @@ -273,6 +319,8 @@ or the network. For example:
else:
# do something else
Splice & Resolve
~~~~~~~~~~~~~~~~

The *splice* function will replace any sigils found in the string with the
actual values from the context. Returns the interpolated string.
Expand All @@ -289,8 +337,10 @@ actual values from the context. Returns the interpolated string.
assert result == "arthexis: /home/arth/webapp"
All keys in the context mapping should be strings (behavior is undefined if not)
The use of uppercase keys is STRONGLY recommended but not required.
Values can be anything, a string, a number, a list, a dict, or an ORM instance.
and will be automatically converted to uppercase.

Values can be anything: a string, a number, a list, a dict, or an ORM instance,
anything that can self-serialize to text with the *str* function works.

.. code-block:: python
Expand All @@ -303,11 +353,28 @@ Values can be anything, a string, a number, a list, a dict, or an ORM instance.
):
assert splice("[[MODEL.OWNER.UPPER]]") == "ARTHEXIS"
If the value cannot or should not self-serialize, you can pass a function
to use as the serializer. The function will be called with the value as-is.

.. code-block:: python
class Model:
owner = "arthexis"
def serializer(model):
return f"owner: {model.owner}"
with context(
MODEL: Model, # [[MODEL.OWNER]]
):
assert splice("[[MODEL]]", seralizer=serializer) == "owner: arthexis"
You can pass additional context to splice directly:

.. code-block:: python
assert splice("[[NAME.UPPER]]", context={"NAME": "arth"}) == "ARTH"
assert splice("[[NAME.UPPER]]", **{"NAME": "arth"}) == "ARTH"
By default, the splice function will recurse into the found values,
interpolating any sigils found in them. This can be disabled by setting
Expand All @@ -325,9 +392,40 @@ the recursion parameter to 0. Default recursion is 6.
result = splice("[[USERNAME]]: [[SETTINGS.BASE_DIR]]", recursion=1)
assert result == "arthexis: /home/[[USERNAME]]"
The function *resolve* is an alias for splice that never recurses.

Vanish & Unvanish
~~~~~~~~~~~~~~~~~

The *vanish* function doesn't resolve sigils, instead it replaces them
with another pattern of text and extracts all the sigils that were replaced
to a map. This can be used for debugging, logging, async processing,
or to sanitize user input that might contain sigils.

.. code-block:: python
from sigils import vanish
text, sigils = vanish("select * from users where username = [[USER]]", "?")
assert text == "select * from users where username = ?"
assert sigils == ["[[USER]]"]
The *unvanish* function does the opposite of vanish, replacing the
vanishing pattern with the sigils.

.. code-block:: python
from sigils import vanish, unvanish
text, sigils = vanish("select * from users where username = [[USER]]", "?")
assert text == "select * from users where username = ?"
assert sigils == ["[[USER]]"]
assert unvanish(text, sigils) == "select * from users where username = [[USER]]"
Execute
~~~~~~~

The *execute* function is similar to resolve, but executes the found text
as a python block (not an expression). This is useful for interpolating code:
Expand All @@ -344,7 +442,6 @@ as a python block (not an expression). This is useful for interpolating code:
assert result == "arthexis"
result = execute("print([[SETTING.BASE_DIR]])")
assert result == "/home/arth/webapp"
Sigils will only be resolved within strings inside the code unless
the unsafe flag is set to True. For example:
Expand All @@ -360,20 +457,6 @@ the unsafe flag is set to True. For example:
assert result == "arthexis"
The *vanish* function doesn't resolve sigils, instead it replaces them
with another pattern of text and extracts all the sigils that were replaced
to a map. This can be used for debugging, logging, async processing,
or to sanitize user input that might contain sigils.

.. code-block:: python
from sigils import vanish
text, sigils = vanish("select * from users where username = [[USER]]", "?")
assert text == "select * from users where username = ?"
assert sigils == ["[[USER]]"]
Async & Multiprocessing
-----------------------

Expand All @@ -382,14 +465,12 @@ them in loops, conditionals, and other control structures. For example:

.. code-block:: python
from sigils import execute, context
from sigils import execute, splice, context
with context(
USERNAME="arthexis",
SETTING={"BASE_DIR": "/home/arth/webapp"},
):
result = execute("if [[USERNAME]] == 'arthexis': print('yes')")
assert result == "yes"
with context("context.json"):
if result := execute("if '[[USERNAME]]' == 'arthexis': print('[[GROUP]]')"):
assert splice("[[USERNAME]]") == "arthexis"
assert result == "Administrators"
This can also make it more efficient to resolve documents with many sigils,
Expand All @@ -399,6 +480,29 @@ instead of resolving each one individually.
Django Integration
------------------

There are several ways to integrate sigils with Django:

- Use the *context* decorator to set the context for a view.
- Use the *resolve* template tag to resolve sigils in templates.

Context Decorator
~~~~~~~~~~~~~~~~~

The *context* decorator can be used to set the context for a view.
For example, to set the context for a view in *views.py*:

.. code-block:: python
from sigils import context
@context(
USERNAME="arthexis",
SETTING={"BASE_DIR": "/home/arth/webapp"},
)
def my_view(request):
...
You can create a `simple tag`_ to resolve sigils in templates.
Create *<your_app>/templatetags/sigils.py* with the following code:

Expand Down Expand Up @@ -473,10 +577,6 @@ or files, with a given context. Example usage:
$ sigils "Hello, [[USERNAME]]!"
# arthexis/sigils
$ sigils -c "USERNAME=arthexis" README.md -o README2.md
$ cat README2.md
# arthexis/sigils
Project Dependencies
--------------------
Expand Down
13 changes: 8 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"

[project]
name = "sigils"
version = "0.2.3"
version = "0.2.5"
authors = [
{name = "Rafael Jesús Guillén Osorio", email = "arthexis@gmail.com"},
]
description = "Manage context-less text with [[SIGILS]]."
readme = "README.rst"
requires-python = ">=3.9"
keywords = ["utils", "sigils", "string", "text", "magic", "context"]
license = {file = "LICENSE"}
license = {text = "MIT"}
classifiers = [
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
Expand All @@ -31,19 +31,22 @@ dependencies = [
'lark',
'lark-parser',
'click',
'pyyaml',
]

[project.urls]
Source = "https://github.com/arthexis/sigils"
Bug-Tracker = "https://github.com/arthexis/sigils/issues"


[project.optional-dependencies]
django = ["Django>=3.2"]
django = [
"Django>=3.2"
]
dev = [
"pytest",
"pytest-cov",
"python-dotenv"
"python-dotenv",
"tox",
]

[project.scripts]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ build
twine
python-dotenv
click
pyyaml
29 changes: 7 additions & 22 deletions sigils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,12 @@
import pathlib
import click

from .sigils import splice, execute, local_context
from .tools import splice, execute
from .contexts import context as local_context

logging.basicConfig(level=logging.WARNING)


# Function that recursively changes the keys of a dictionary
# to uppercase. This is used to convert the context dictionary
def _upper_keys(d):
if isinstance(d, dict):
return {k.upper(): _upper_keys(v) for k, v in d.items()}
else:
return d


@click.command()
@click.argument("text", nargs=-1)
@click.option("--on-error", "-e",
Expand All @@ -27,24 +19,16 @@ def _upper_keys(d):
@click.option("--interactive", "-i", is_flag=True, help="Enter interactive mode.")
@click.option("--file", "-f", help="Read text from given file.")
@click.option("--exec", "-x", is_flag=True, help="Execute the text after resolving sigils.")
@click.option("--context", "-c", help="Context to use for resolving sigils.")
@click.option("--context", "-c", multiple=True,
help="Context to use for resolving sigils."
"Can be a file or a key=value pair. Multiple values are allowed.")
def main(text, on_error, verbose, default, interactive, file, exec, context):
"""Resolve sigils found in the given text."""
_context = {}

if verbose == 1:
logging.basicConfig(level=logging.INFO)
elif verbose > 1:
logging.basicConfig(level=logging.DEBUG)

if context:
# If the context looks like a TOML file, load it
if context.endswith(".toml"):
import tomllib
with open(context, 'r') as f:
toml_data = tomllib.loads(f.read())
_context.update(_upper_keys(toml_data))

if interactive:
import code
def readfunc(prompt):
Expand All @@ -61,7 +45,8 @@ def readfunc(prompt):
text = f.read()
else:
text = " ".join(text)
with local_context(**_context):
print(f"Context: {context}")
with local_context(*context):
if not exec:
result = splice(text, on_error=on_error, default=default)
else:
Expand Down
Loading

0 comments on commit 00d59d4

Please sign in to comment.