Sphinx - Let the customize HTML know the variable - python

# conf.py
language='en'
html_extra_path = ["customize.html"]
<!-- customize.html -->
{{ variables }} <!-- from the conf.py -->
{{ language }} <!-- expected output: en -->
How can I let the customize.html know the variables is from the config?
.. note:: assume the customize.html file is not in the documents of the theme.
I can do it by myself with Jinja, but this is not what I want.
I think sphinx already provides a way to do the things, does anyone know what is it?

Solution 1: modify sphinx-build.exe process
I hack the code (i.e. you could not build with sphinx-build.exe directly) to achieve it.
First, we observe sphinx-build.exe do what things.
# site-packages\Sphinx-x.x.x.dist-info\entry_points.txt
[console_scripts]
...
sphinx-build = sphinx.cmd.build:main
...
and then you know it actually calls sphinx.cmd.build:main to run,
you can reference it and make modifications to satisfying you.
For example:
import sphinx.cmd.build
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.cmd.build import patch_docutils, docutils_namespace, handle_exception, Sphinx
def setup_extra_html(app):
html_builder = app.builder
ctx = {attr: app.config[attr] for attr in dir(app.config) if not attr.startswith('_')}
html_builder.globalcontext = ctx.copy()
# Please put your HTML to the ``templates_path`` that you define, since it concept about the BuiltinTemplateLoader.pathchain
pagename = 'disqus_statistic' # <-- your HTML, you can set it on the conf.py and then get it with ``ctx``
templatename = f'{pagename}.html'
html_builder.handle_page(pagename=pagename, addctx=dict(), templatename=templatename, outfilename=None)
def your_build_main(*args):
...
try:
with patch_docutils(source_dir)), docutils_namespace():
app = Sphinx(...)
if isinstance(app.builder, StandaloneHTMLBuilder):
setup_extra_html(app)
app.build(force_all=False, filenames)
return app.statuscode
except (Exception, KeyboardInterrupt) as exc:
...
cmd_list = [source_dir, output_dir, '-b', 'html', ...]
sphinx.cmd.build.build_main = your_build_main # override it.
sphinx.cmd.build.main(cmd_list) # it will call sphinx.cmd.build.build_main
And now, the following contents will work as you expected.
<!-- original disqus_statistic.html -->
{%- if html_favicon %}
Test Icon
{%- endif %}
{{ language }}
you should complete the detail by yourself since the code is too long.
or you can refer my script of sphinx_cmd_build.py
Solution 2: add the plugin (extension)
tl;dr
# your_extension.py
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
import pathlib
def expand_init_builder(app):
Sphinx._init_builder(app)
do_something(app)
def setup(app: Sphinx):
app.add_config_value('config_value_define_by_you', default='', rebuild=True)
app._init_builder = lambda: expand_init_builder(app)
def do_something(app: Sphinx):
user_config = {attr: app.config[attr] for attr in dir(app.config) if not attr.startswith('_')} # all variable of conf.py
# user_config.update(...) # Hard coding is fine, but not recommend.
user_config.update(dict(Path=pathlib.Path)) # recommend you, it's useful.
html_builder: StandaloneHTMLBuilder = app.builder
html_builder.globalcontext = user_config
html_builder.handle_page(pagename=page_name, addctx=dict(), templatename=template_name, outfilename=None)
# conf.py
# sys.path.insert(...)
extensions.append('your_extension') # Make sure your scrips can found in sys.path.
# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-templates_path
templates_path = ['_templates/sphinx_rtd_theme'] # I hope you know what I mean... see the above link.
# I put my_html.html in ``_templates/sphinx_rtd_theme/my_html.html``
config_value_define_by_you = "https://github.com/CarsonSlovoka/typing-game"
<!-- my_html.html -->
{{ Path(config_value_define_by_you).name }} <!-- render result: typing-game -->
Long story (explain solutions2)
Sphinx-build.exe doing what things?
init # <-- and this one is not we cared.
create an instance of Sphinx(), the instance is app. i.e. app = Sphinx(...)
app.build
it calls the builder to start building, the builder format which defines by the user, in my case the build format is HTML, so its builder is StandaloneHTMLBuilder
And then, you know all files that are created by the builder.
The idea is: if we can get the builder, then we can do anything we want.
You will find the builder that is creat after the Sphinx(...),
so the solutions one, I tell you setup_extra_html after the app = Sphinx(...)
If you don't like to write these codes and think it's too complex.
The second way is to write the extensions,
the concept is the same as the above -- try to get the builder
you see the Sphinx(...) its constructor, and you find the code as below
class Sphinx:
def __init__(...):
...
# load all user-given extension modules
for extension in self.config.extensions:
self.setup_extension(extension) # <-- the extension you write
...
# create the builder
self.builder = self.create_builder(buildername) <-- this is we want
self._init_env(freshenv)
self._init_builder()
And then, you know the normal way to create the extensions which couldn't get the builder,
but you notice that if you do something after the self._init_builder() finished, then it's ok.
I provide my projects for your reference.
The real things that I want is, I want to create a page and I hope it can count the numbers of comments for each article, and show me.
You will understand, if I don't use the sphinx, but choose naming convention, then I must hard code a lot of things.
solution 1: https://github.com/CarsonSlovoka/typing-game/commit/376c9e20eac4a3c9b269b5bfbc8adb85ad9f6d36
solution 2: https://github.com/CarsonSlovoka/typing-game/commit/69411e15f1ace853edcafafc14759ba79b7ac288
demo: https://carsonslovoka.github.io/typing-game/en/doc.html#statistic -> and then, click counts of the Disqus.
original HTML
I hope you will get help and think it useful!

Related

Autoprefixer Filter Not Working in Flask_Assets

I have tried to get the autoprefixer filter to work with flask_assets by following the instructions in the Flask_Assets documentation, but it does not appear to apply the filter. Here is my code:
# construct flask app object
from flask import Flask, render_template_string
flask_args = { 'import_name': __name__ }
flask_app = Flask(**flask_args)
from flask_assets import Environment, Bundle
assets = Environment(flask_app)
assets.config['AUTOPREFIXER_BIN'] = 'postcss'
assets.config['AUTOPREFIXER_BROWSERS'] = [ '> 1%' ]
css_min = Bundle('../styles/mycss.css', filters='autoprefixer', output='styles/test.css')
assets.register('css_assets', css_min)
#flask_app.route('/')
def landing_page():
html = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\
<head>{% assets "css_assets" %}\
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">\
{% endassets %}\
<title>Hello</title>\
</head>\
<h1>Hello World</h1>\
<p>Just a test of flask</p>'
return render_template_string(html), 200
if __name__ == '__main__':
flask_app.run(host='0.0.0.0', port=5000)
I have been able to apply the cssmin, pyscss, uglifyjs and jsmin filters successfully. I can also run autoprefixer on the command line to successfully compile a transformed output:
postcss --use autoprefixer --autoprefixer.browsers "> 1%" -o test.css mycss.css
However, when trying to run autoprefixer through flask_assets registration, the process neither throws an error nor does it seem to take the required time to compile. It does produce the output file but when I examine the resulting file, none of the prefixes have been applied.
UPDATE: This problem seems to occur whenever attempting to configure options for ANY filter. I have not been able to get uglifyjs to accept 'UGLIFYJS_EXTRA_ARGS' or for the pyscss filter to adopt a new style using 'PYSCSS_STYLE' either. I have tried to set these configuration as environmental variables using os.environ['AUTOPREFIXER_BIN'] as well as attempting to pass them through flask.config['AUTOPREFIXER_BIN']. But none of the configuration settings have been applied when the filter is run. It is also not clear to me where in the code itself the configuration options are constructed by either Bundle or Environment.
One SO post claims to have found a way to get a configuration setting to work, but the post does not show the entire workflow of how flask_assets needs to be setup to ingest these options.
Perhaps someone can help me understand what I am doing wrong?
Autoprefixer:
There is nothing wrong with your code1. You are just not using the correct filter for the latest version of Autoprefixer. If you look at the history of the releases in that link, since version 6.0.0, it started using postcss. Your code will work for versions older than 6.0.0.
Webassets has provided support for versions after 6.0.0 (inclusive), by providing the autoprefixer6 filter.
Therefore all you have to do is change the filter(s) while initializing your bundle, like so:
css_min = Bundle('../styles/mycss.css', filters='autoprefixer6', output='styles/test.css')
Other Filters' Configurations:
Don't use os.environ, that is not the way to set configuration variables for Flask and flask-extensions. The most common (and preferred) way to set configuration for extensions is by using the flask Config itself, and in large projects this is done using a separate config file. The extensions will pickup its configuration options from flask's config.
Depending on which extension you use, you can also set the config separately like you have done, but that is rarely used, from what I have seen so far.
Please check the Flask's Configuration related documentation for some good examples on how to setup configuration for your app "properly".
from flask import Flask, render_template_string
from flask_assets import Environment, Bundle
# construct flask app object
flask_args = {'import_name': __name__}
flask_app = Flask(**flask_args)
assets = Environment(flask_app)
# specify the bin path (optional), required only if not globally installed
assets.config['AUTOPREFIXER_BIN'] = 'path/to/postcss'
assets.config['AUTOPREFIXER_BROWSERS'] = ['> 1%', ]
# use the autoprefixer6 updated filter
css_min = Bundle('../styles/mycss.css', filters='autoprefixer6',
output='styles/test.css')
assets.register('css_assets', css_min)
#flask_app.route('/')
def landing_page():
html = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\
<head>{% assets "css_assets" %}\
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">\
{% endassets %}\
<title>Hello</title>\
</head>\
<h1>Hello World</h1>\
<p>Just a test of flask</p>'
return render_template_string(html), 200
if __name__ == '__main__':
flask_app.run(host='0.0.0.0', port=5000)
Remember to clean out the previously generated files, if the source css/js has not changed, i.e remove the output files, and the .webassets-cache folder.
1Except for code style & formatting conventions!

Can I make a file optional based on a variable's value in cookiecutter.json

I would like to have a file that is optionally added in my python cookiecutter project.
An example would be in cookiecutter.json have the variable
{"settings_file": true}
which would create a file settings.py at the root of my directory (with maybe some contents).
Does cookiecutter offer an option to do this? Or should I be using the post processing hook to write a script which creates the files (which I feel like is not the most elegant solution).
I'm going to answer my own question, in case someone runs into it: it is not yet implemented as a feature of the project, see ticket for enhancement here: https://github.com/audreyr/cookiecutter/issues/127
The "least ugly" solution I have come up with is to create the files every time, and clean them up during the post hook (you could also create them during the post hook, but would lose cookiecutter templating advantages)
According to the official docs, you can use a post-generate hook script (hooks/post_gen_project.py)
(Copying the snippet here for those skimming through)
import os
import sys
REMOVE_PATHS = [
'{% if cookiecutter.packaging != "pip" %} requirements.txt {% endif %}',
'{% if cookiecutter.packaging != "poetry" %} poetry.lock {% endif %}',
]
for path in REMOVE_PATHS:
path = path.strip()
if path and os.path.exists(path):
if os.path.isdir(path):
os.rmdir(path)
else:
os.unlink(path)
It is possible to use Jinja templating and conditionals in file names.
As such you can have a file named {% if cookiecutter.settings_file == "true" -%} __main__.py {%- endif %} which will only be created if settings_file is true.
Note that boolean variables are not yet supported in Cookiecutter, although there is a merged PR to add this functionality. One way to solve it now is to use e.g., ["true", "false"] as values.

How to put css files to head by means UIModule in the tornado?

I want to add set of css files to head. For instance,
python file:
modules.py
class SomeModule(tornado.web.UIModule):
def css_files(self):
return [self.handler.static_url('css/modules/some-module.css'),]
def render(self, some_data=None):
result = ''
if some_data is not None:
"""to do something with data"""
return result
server.py
app = Application(
...
ui_module=modules
...
)
template file:
...
{% module SomeModule(some_data=put_data_here) %}
As the result, I see only data that were returned from render. But css files weren't set between head tags.
What's the result of the template file depends on how you generate the template file. As far as I can see, you didn't include the code for that in your question.
If you want to use css_files() or similar, you need to use self.render_string() in order to give Tornado a chance to insert the CSS tags in the proper places.
For an example of how to use tornado.web.UIModule, see this Slideshare presentation

Is inline code allowed in Jinja templates?

I'm using Jinja on my site and I like it.
I've come across a simple need. How to display today's date? Is there a way to inline some Python code in a Jinja template?
import datetime
now = datetime.datetime.utcnow()
print now.strftime("%Y-%m-%d %H:%M")
This article says no, but suggests using a macro or a filter?
Really? Must we resort to all that? OK, what would that look like in this case?
No, there is no way to inline Python into Jinja. However, you can add to the constructs that Jinja knows by extending the Environment of the template engine or the global namespace available to all templates. Alternately, you can add a filter that let's you format datetime objects.
Flask stores the Jinja2 Environment on app.jinja_env. You can inject new context into the environment by either adding to this dictionary directly, or by using the #app.context_processor decorator.
Whatever path you choose, this should be done while you are setting up the application, before you have served any requests. (See the snippets section of the website for some good examples of how to set up filters - the docs contain a good example of adding to the global variables).
The current answers are correct for pretty much every situation. However there are some very rare cases where you would want to have python code inside the template. In my case I want to use it to preprocess some latex files and I would prefer to keep the python code generating table values, plots, etc, inside the latex file it self.
So I made a Jinja2 extension that adds a new "py" block allowing python code to be written inside the template. Please keep in mind that I had to do some questionable work-arounds to get this to work, so I'm not 100% sure in which situations it fails or behaves unexpectedly.
This is an example template.
Foo was given to the template
foo: {{ foo }}
Bar was not, so it is missing
bar is missing: {{ bar == missing }}
{% py %}
# Normal python code in here
# Excess indentation will be removed.
# All template variables are accessible and can be modified.
import numpy as np
a = np.array([1, 2])
m = np.array([[3, 4], [5, 6]])
bar = m # a * foo
# It's also possible to template the python code.
{% if change_foo %}
foo = 'new foo value'
{% endif %}
print("Stdio is redirected to the output.")
{% endpy %}
Foo will have the new value if you set change_foo to True
foo: {{ foo }}
Bar will now have a value.
bar: {{ bar }}
{% py %}
# The locals from previous blocks are accessible.
m = m**2
{% endpy %}
m:
{{ m }}
The output if we set the template parameters to foo=10, change_foo=True is:
Foo was given to the template
foo: 10
Bar was not, so it is missing
bar is missing: True
Stdio is redirected to the output.
Foo will have the new value if you set change_foo to True
foo: new foo value
Bar will now have a value.
bar: [110 170]
m:
[[ 9 16]
[25 36]]
The extension with a main function to run the example.
from jinja2 import Environment, PackageLoader, nodes
from jinja2.ext import Extension
from textwrap import dedent
from io import StringIO
import sys
import re
import ctypes
def main():
env = Environment(
loader=PackageLoader('python_spike', 'templates'),
extensions=[PythonExtension]
)
template = env.get_template('emb_py2.txt')
print(template.render(foo=10, change_foo=True))
var_name_regex = re.compile(r"l_(\d+)_(.+)")
class PythonExtension(Extension):
# a set of names that trigger the extension.
tags = {'py'}
def __init__(self, environment: Environment):
super().__init__(environment)
def parse(self, parser):
lineno = next(parser.stream).lineno
body = parser.parse_statements(['name:endpy'], drop_needle=True)
return nodes.CallBlock(self.call_method('_exec_python',
[nodes.ContextReference(), nodes.Const(lineno), nodes.Const(parser.filename)]),
[], [], body).set_lineno(lineno)
def _exec_python(self, ctx, lineno, filename, caller):
# Remove access indentation
code = dedent(caller())
# Compile the code.
compiled_code = compile("\n"*(lineno-1) + code, filename, "exec")
# Create string io to capture stdio and replace it.
sout = StringIO()
stdout = sys.stdout
sys.stdout = sout
try:
# Execute the code with the context parents as global and context vars and locals.
exec(compiled_code, ctx.parent, ctx.vars)
except Exception:
raise
finally:
# Restore stdout whether the code crashed or not.
sys.stdout = stdout
# Get a set of all names in the code.
code_names = set(compiled_code.co_names)
# The the frame in the jinja generated python code.
caller_frame = sys._getframe(2)
# Loop through all the locals.
for local_var_name in caller_frame.f_locals:
# Look for variables matching the template variable regex.
match = re.match(var_name_regex, local_var_name)
if match:
# Get the variable name.
var_name = match.group(2)
# If the variable's name appears in the code and is in the locals.
if (var_name in code_names) and (var_name in ctx.vars):
# Copy the value to the frame's locals.
caller_frame.f_locals[local_var_name] = ctx.vars[var_name]
# Do some ctypes vodo to make sure the frame locals are actually updated.
ctx.exported_vars.add(var_name)
ctypes.pythonapi.PyFrame_LocalsToFast(
ctypes.py_object(caller_frame),
ctypes.c_int(1))
# Return the captured text.
return sout.getvalue()
if __name__ == "__main__":
main()
You can add to global variables which can be accessed from Jinja templates. You can put your own function definitions in there, which do whatever you need.

How Do I Suppress or Disable Warnings in reSTructuredText?

I'm working on a CMS in Python that uses reStructuredText (via docutils) to format content. Alot of my content is imported from other sources and usually comes in the form of unformatted text documents. reST works great for this because it makes everything look pretty sane by default.
One problem I am having, however, is that I get warnings dumped to stderr on my webserver and injected into my page content. For example, I get warnings like the following on my web page:
System Message: WARNING/2 (, line 296); backlink
My question is: How do I suppress, disable, or otherwise re-direct these warnings?
Ideally, I'd love to write these out to a log file, but if someone can just tell me how to turn off the warnings from being injected into my content then that would be perfect.
The code that's responsible for parsing the reST into HTML:
from docutils import core
import reSTpygments
def reST2HTML( str ):
parts = core.publish_parts(
source = str,
writer_name = 'html')
return parts['body_pre_docinfo'] + parts['fragment']
def reST2HTML( str ):
parts = core.publish_parts(
source = str,
writer_name = 'html',
settings_overrides={'report_level':'quiet'},
)
return parts['body_pre_docinfo'] + parts['fragment']
It seems the report_level accept string is an old version. Now, the below is work for me.
import docutils.core
import docutils.utils
from pathlib import Path
shut_up_level = docutils.utils.Reporter.SEVERE_LEVEL + 1
docutils.core.publish_file(
source_path=Path(...), destination_path=Path(...),
settings_overrides={'report_level': shut_up_level},
writer_name='html')
about level
# docutils.utils.__init__.py
class Reporter(object):
# system message level constants:
(DEBUG_LEVEL,
INFO_LEVEL,
WARNING_LEVEL,
ERROR_LEVEL,
SEVERE_LEVEL) = range(5)
...
def system_message(self, level, message, *children, **kwargs):
...
if self.stream and (level >= self.report_level # self.report_level was set by you. (for example, shut_up_level)
or self.debug_flag and level == self.DEBUG_LEVEL
or level >= self.halt_level):
self.stream.write(msg.astext() + '\n')
...
return msg
According to the above code, you know that you can assign the self.report_level (i.e. settings_overrides={'report_level': ...}) let the warning not show.
and I set it to SERVER_LEVEL+1, so it will not show any error. (you can set it according to your demand.)

Categories

Resources