[FIX] update py.js to 0.6, port existin classes to it, attempt to implement timedelta

bzr revid: xmo@openerp.com-20121008120619-lhmexjnujjrigjn9
This commit is contained in:
Xavier Morel 2012-10-08 14:06:19 +02:00
parent 10bc6ddfe4
commit 7bcbadb099
15 changed files with 1798 additions and 765 deletions

View File

@ -1,5 +1,5 @@
repo: 076b192d0d8ab2b92d1dbcfa3da055382f30eaea
node: 87e977311edbbb5f281b87390a9a304eb194ce89
node: e47d717cf47d165a5a9916abbb5ceb138661efc6
branch: default
latesttag: 0.5
latesttagdistance: 15
latesttag: 0.6
latesttagdistance: 1

View File

@ -1,14 +1,7 @@
What
====
``py.js`` is a parser and evaluator of Python expressions, written in
pure javascript.
``py.js`` is not intended to implement a full Python interpreter
(although it could be used for such an effort later on), its
specification document is the `Python 2.7 Expressions spec
<http://docs.python.org/reference/expressions.html>`_ (along with the
lexical analysis part).
Syntax
------
@ -69,7 +62,7 @@ Data model protocols
``py.js`` currently implements the following protocols (or
sub-protocols) of the `Python 2.7 data model
<http://docs.python.org/reference/datamodel.html>`_:
<>`_:
Rich comparisons
Pretty much complete (including operator fallbacks), although the

View File

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyjs.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyjs.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/pyjs"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyjs"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -0,0 +1,53 @@
.. default-domain: python
.. _builtins:
Supported Python builtins
=========================
.. function:: py.type(object)
Gets the class of a provided object, if possible.
.. note:: currently doesn't work correctly when called on a class
object, will return the class itself (also, classes
don't currently have a type).
.. js:function:: py.type(name, bases, dict)
Not exactly a builtin as this form is solely javascript-level
(currently). Used to create new ``py.js`` types. See :doc:`types`
for its usage.
.. data:: py.None
.. data:: py.True
.. data:: py.False
.. data:: py.NotImplemented
.. class:: py.object
Base class for all types, even implicitly (if no bases are
provided to :js:func:`py.type`)
.. class:: py.bool([object])
.. class:: py.float([object])
.. class:: py.str([object])
.. class:: py.unicode([object])
.. class:: py.tuple()
.. class:: py.list()
.. class:: py.dict()
.. function:: py.isinstance(object, type)
.. function:: py.issubclass(type, other_type)
.. class:: py.classmethod

View File

@ -0,0 +1,247 @@
# -*- coding: utf-8 -*-
#
# py.js documentation build configuration file, created by
# sphinx-quickstart on Sun Sep 9 19:36:23 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.todo']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'py.js'
copyright = u'2012, Xavier Morel'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.6'
# The full version, including alpha/beta/rc tags.
release = '0.6'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# Default sphinx domain
default_domain = 'js'
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# default code-block highlighting
highlight_language = 'javascript'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'pyjsdoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'pyjs.tex', u'py.js Documentation',
u'Xavier Morel', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'pyjs', u'py.js Documentation',
[u'Xavier Morel'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'pyjs', u'py.js Documentation',
u'Xavier Morel', 'pyjs', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'

View File

@ -0,0 +1,64 @@
Differences with Python
=======================
* ``py.js`` completely ignores old-style classes as well as their
lookup details. All ``py.js`` types should be considered matching
the behavior of new-style classes
* New types can only have a single base. This is due to ``py.js``
implementing its types on top of Javascript's, and javascript being
a single-inheritance language.
This may change if ``py.js`` ever reimplements its object model from
scratch.
* Piggybacking on javascript's object model also means metaclasses are
not available (:js:func:`py.type` is a function)
* A python-level function (created through :js:class:`py.PY_def`) set
on a new type will not become a method, it'll remain a function.
* :js:func:`py.PY_parseArgs` supports keyword-only arguments (though
it's a Python 3 feature)
* Because the underlying type is a javascript ``String``, there
currently is no difference between :js:class:`py.str` and
:js:class:`py.unicode`. As a result, there also is no difference
between :js:func:`__str__` and :js:func:`__unicode__`.
Unsupported features
--------------------
These are Python features which are not supported at all in ``py.js``,
usually because they don't make sense or there is no way to support them
* The ``__delattr__``, ``__delete__`` and ``__delitem__``: as
``py.js`` only handles expressions and these are accessed via the
``del`` statement, there would be no way to call them.
* ``__del__`` the lack of cross-platform GC hook means there is no way
to know when an object is deallocated.
* ``__slots__`` are not handled
* Dedicated (and deprecated) slicing special methods are unsupported
Missing features
----------------
These are Python features which are missing because they haven't been
implemented yet:
* Class-binding of descriptors doesn't currently work.
* Instance and subclass checks can't be customized
* "poor" comparison methods (``__cmp__`` and ``__rcmp__``) are not
supported and won't be falled-back to.
* ``__coerce__`` is currently supported
* Context managers are not currently supported
* Unbound methods are not supported, instance methods can only be
accessed from instances.

View File

@ -0,0 +1,161 @@
.. py.js documentation master file, created by
sphinx-quickstart on Sun Sep 9 19:36:23 2012.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
py.js, a Python expressions parser and evaluator
================================================
``py.js`` is a parser and evaluator of Python expressions, written in
pure javascript.
``py.js`` is not intended to implement a full Python interpreter, its
specification document is the `Python 2.7 Expressions spec
<http://docs.python.org/reference/expressions.html>`_ (along with the
lexical analysis part) as well as the Python builtins.
.. toctree::
:maxdepth: 2
builtins
types
utility
differences
Usage
-----
To evaluate a Python expression, simply call
:func:`py.eval`. :func:`py.eval` takes a mandatory Python expression
parameter, as a string, and an optional evaluation context (namespace
for the expression's free variables), and returns a javascript value::
> py.eval("t in ('a', 'b', 'c') and foo", {t: 'c', foo: true});
true
If the expression needs to be repeatedly evaluated, or the result of
the expression is needed in its "python" form without being converted
back to javascript, you can use the underlying triplet of functions
:func:`py.tokenize`, :func:`py.parse` and :func:`py.evaluate`
directly.
API
---
Core functions
++++++++++++++
.. function:: py.eval(expr[, context])
"Do everything" function, to use for one-shot evaluation of Python
expressions. Chains tokenizing, parsing and evaluating the
expression then :ref:`converts the result back to javascript
<convert-js>`
:param expr: Python expression to evaluate
:type expr: String
:param context: evaluation context for the expression's free
variables
:type context: Object
:returns: the expression's result, converted back to javascript
.. function:: py.tokenize(expr)
Expression tokenizer
:param expr: Python expression to tokenize
:type expr: String
:returns: token stream
.. function:: py.parse(tokens)
Parses a token stream and returns the corresponding parse tree.
The parse tree is stateless and can be memoized and reused for
frequently evaluated expressions.
:param tokens: token stream from :func:`py.tokenize`
:returns: parse tree
.. function:: py.evaluate(tree[, context])
Evaluates the expression represented by the provided parse tree,
using the provided context for the exprssion's free variables.
:param tree: parse tree returned by :func:`py.parse`
:param context: evaluation context
:returns: the "python object" resulting from the expression's
evaluation
:rtype: :class:`py.object`
.. _convert-py:
Conversions from Javascript to Python
+++++++++++++++++++++++++++++++++++++
``py.js`` will automatically attempt to convert non-:class:`py.object`
values into their ``py.js`` equivalent in the following situations:
* Values passed through the context of :func:`py.eval` or
:func:`py.evaluate`
* Attributes accessed directly on objects
* Values of mappings passed to :class:`py.dict`
Notably, ``py.js`` will *not* attempt an automatic conversion of
values returned by functions or methods, these must be
:class:`py.object` instances.
The automatic conversions performed by ``py.js`` are the following:
* ``null`` is converted to :data:`py.None`
* ``true`` is converted to :data:`py.True`
* ``false`` is converted to :data:`py.False`
* numbers are converted to :class:`py.float`
* strings are converted to :class:`py.str`
* functions are wrapped into :class:`py.PY_dev`
* ``Array`` instances are converted to :class:`py.list`
The rest generates an error, except for ``undefined`` which
specifically generates a ``NameError``.
.. _convert-js:
Conversions from Python to Javascript
+++++++++++++++++++++++++++++++++++++
py.js types (extensions of :js:class:`py.object`) can be converted
back to javascript by calling their :js:func:`py.object.toJSON`
method.
The default implementation raises an error, as arbitrary objects can
not be converted back to javascript.
Most built-in objects provide a :js:func:`py.object.toJSON`
implementation out of the box.
Javascript-level exceptions
+++++++++++++++++++++++++++
Javascript allows throwing arbitrary things, but runtimes don't seem
to provide any useful information (when they ever do) if what is
thrown isn't a direct instance of ``Error``. As a result, while
``py.js`` tries to match the exception-throwing semantics of Python it
only ever throws bare ``Error`` at the javascript-level. Instead, it
prefixes the error message with the name of the Python expression, a
colon, a space, and the actual message.
For instance, where Python would throw ``KeyError("'foo'")`` when
accessing an invalid key on a ``dict``, ``py.js`` will throw
``Error("KeyError: 'foo'")``.
.. _Python Data Model: http://docs.python.org/reference/datamodel.html

View File

@ -0,0 +1,190 @@
@ECHO OFF
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
set I18NSPHINXOPTS=%SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. singlehtml to make a single large HTML file
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. devhelp to make HTML files and a Devhelp project
echo. epub to make an epub
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. text to make text files
echo. man to make manual pages
echo. texinfo to make Texinfo files
echo. gettext to make PO message catalogs
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "singlehtml" (
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pyjs.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pyjs.ghc
goto end
)
if "%1" == "devhelp" (
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
if errorlevel 1 exit /b 1
echo.
echo.Build finished.
goto end
)
if "%1" == "epub" (
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The epub file is in %BUILDDIR%/epub.
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
if errorlevel 1 exit /b 1
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "text" (
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The text files are in %BUILDDIR%/text.
goto end
)
if "%1" == "man" (
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The manual pages are in %BUILDDIR%/man.
goto end
)
if "%1" == "texinfo" (
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
goto end
)
if "%1" == "gettext" (
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
if errorlevel 1 exit /b 1
echo.
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
if errorlevel 1 exit /b 1
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
if errorlevel 1 exit /b 1
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View File

@ -0,0 +1,248 @@
Implementing a custom type
==========================
To implement a custom python-level type, one can use the
:func:`py.type` builtin. At the JS-level, it is a function with the
same signature as the :py:class:`type` builtin [#bases]_. It returns a
child type of its one base (or :py:class:`py.object` if no base is
provided).
The ``dict`` parameter to :func:`py.type` can contain any
attribute, javascript-level or python-level: the default
``__getattribute__`` implementation will ensure they are converted to
Python-level attributes if needed. Most methods are also wrapped and
converted to :ref:`types-methods-python`, although there are a number
of special cases:
* Most "magic methods" of the data model ("dunder" methods) remain
javascript-level. See :ref:`the listing of magic methods and their
signatures <types-methods-dunder>`. As a result, they do not respect
the :ref:`types-methods-python-call`
* The ``toJSON`` and ``fromJSON`` methods are special-cased to remain
javascript-level and don't follow the
:ref:`types-methods-python-call`
* Functions which have been wrapped explicitly (via
:class:`py.PY_def`, :py:class:`py.classmethod` or
:py:class:`py.staticmethod`) are associated to the class
untouched. But due to their wrapper, they will use the
:ref:`types-methods-python-call` anyway
.. _types-methods-python:
Python-level callable
---------------------
Wrapped javascript function *or* the :func:`__call__` method itself
follow the :ref:`types-methods-python-call`. As a result, they can't
(easily) be called directly from javascript code. Because
:func:`__new__` and :func:`__init__` follow from :func:`__call__`,
they also follow the :ref:`types-methods-python-call`.
:func:`py.PY_call` should be used when interacting with them from
javascript is necessary.
Because ``__call__`` follows the :ref:`types-methods-python-call`,
instantiating a ``py.js`` type from javascript requires using
:func:`py.PY_call`.
.. _types-methods-python-call:
Python calling conventions
++++++++++++++++++++++++++
The python-level arguments should be considered completely opaque,
they should be interacted with through :func:`py.PY_parseArgs` (to
extract python-level arguments to javascript implementation code) and
:func:`py.PY_call` (to call :ref:`types-methods-python` from
javascript code).
A callable following the :ref:`types-methods-python-call` *must*
return a ``py.js`` object, an error will be generated when failing to
do so.
.. todo:: arguments forwarding when e.g. overriding methods?
.. _types-methods-dunder:
Magic methods
-------------
``py.js`` doesn't support calling magic ("dunder") methods of the
datamodel from Python code, and these methods remain javascript-level
(they don't follow the :ref:`types-methods-python-call`).
Here is a list of the understood datamodel methods, refer to `the
relevant Python documentation
<http://docs.python.org/reference/datamodel.html?highlight=data%20model#special-method-names>`_
for their roles.
Basic customization
+++++++++++++++++++
.. function:: __hash__()
:returns: String
.. function:: __eq__(other)
The default implementation tests for identity
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __ne__(other)
The default implementation calls :func:`__eq__` and reverses
its result.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __lt__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __le__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __ge__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __gt__(other)
The default implementation simply returns
:data:`py.NotImplemented`.
:param other: :py:class:`py.object` to compare this object with
:returns: :py:class:`py.bool`
.. function:: __str__()
Simply calls :func:`__unicode__`. This method should not be
overridden, :func:`__unicode__` should be overridden instead.
:returns: :py:class:`py.str`
.. function:: __unicode__()
:returns: :py:class:`py.unicode`
.. function:: __nonzero__()
The default implementation always returns :data:`py.True`
:returns: :py:class:`py.bool`
Customizing attribute access
++++++++++++++++++++++++++++
.. function:: __getattribute__(name)
:param String name: name of the attribute, as a javascript string
:returns: :py:class:`py.object`
.. function:: __getattr__(name)
:param String name: name of the attribute, as a javascript string
:returns: :py:class:`py.object`
.. function:: __setattr__(name, value)
:param String name: name of the attribute, as a javascript string
:param value: :py:class:`py.object`
Implementing descriptors
++++++++++++++++++++++++
.. function:: __get__(instance)
.. note:: readable descriptors don't currently handle "owner
classes"
:param instance: :py:class:`py.object`
:returns: :py:class:`py.object`
.. function:: __set__(instance, value)
:param instance: :py:class:`py.object`
:param value: :py:class:`py.object`
Emulating Numeric Types
+++++++++++++++++++++++
* Non-in-place binary numeric methods (e.g. ``__add__``, ``__mul__``,
...) should all be supported including reversed calls (in case the
primary call is not available or returns
:py:data:`py.NotImplemented`). They take a single
:py:class:`py.object` parameter and return a single
:py:class:`py.object` parameter.
* Unary operator numeric methods are all supported:
.. function:: __pos__()
:returns: :py:class:`py.object`
.. function:: __neg__()
:returns: :py:class:`py.object`
.. function:: __invert__()
:returns: :py:class:`py.object`
* For non-operator numeric methods, support is contingent on the
corresponding :ref:`builtins <builtins>` being implemented
Emulating container types
+++++++++++++++++++++++++
.. function:: __len__()
:returns: :py:class:`py.int`
.. function:: __getitem__(name)
:param name: :py:class:`py.object`
:returns: :py:class:`py.object`
.. function:: __setitem__(name, value)
:param name: :py:class:`py.object`
:param value: :py:class:`py.object`
.. function:: __iter__()
:returns: :py:class:`py.object`
.. function:: __reversed__()
:returns: :py:class:`py.object`
.. function:: __contains__(other)
:param other: :py:class:`py.object`
:returns: :py:class:`py.bool`
.. [#bases] with the limitation that, because :ref:`py.js builds its
object model on top of javascript's
<details-object-model>`, only one base is allowed.

View File

@ -0,0 +1,111 @@
Utility functions for interacting with ``py.js`` objects
========================================================
Essentially the ``py.js`` version of the Python C API, these functions
are used to implement new ``py.js`` types or to interact with existing
ones.
They are prefixed with ``PY_``.
.. function:: py.PY_call(callable[, args][, kwargs])
Call an arbitrary python-level callable from javascript.
:param callable: A ``py.js`` callable object (broadly speaking,
either a class or an object with a ``__call__``
method)
:param args: javascript Array of :class:`py.object`, used as
positional arguments to ``callable``
:param kwargs: javascript Object mapping names to
:class:`py.object`, used as named arguments to
``callable``
:returns: nothing or :class:`py.object`
.. function:: py.PY_parseArgs(arguments, format)
Arguments parser converting from the :ref:`user-defined calling
conventions <types-methods-python-call>` to a JS object mapping
argument names to values. It serves the same role as
`PyArg_ParseTupleAndKeywords`_.
::
var args = py.PY_parseArgs(
arguments, ['foo', 'bar', ['baz', 3], ['qux', "foo"]]);
roughly corresponds to the argument spec:
.. code-block:: python
def func(foo, bar, baz=3, qux="foo"):
pass
.. note:: a significant difference is that "default values" will
be re-evaluated at each call, since they are within the
function.
:param arguments: array-like objects holding the args and kwargs
passed to the callable, generally the
``arguments`` of the caller.
:param format: mapping declaration to the actual arguments of the
function. A javascript array composed of five
possible types of elements:
* The literal string ``'*'`` marks all following
parameters as keyword-only, regardless of them
having a default value or not [#kwonly]_. Can
only be present once in the parameters list.
* A string prefixed by ``*``, marks the positional
variadic parameter for the function: gathers all
provided positional arguments left and makes all
following parameters keyword-only
[#star-args]_. ``*args`` is incompatible with
``*``.
* A string prefixed with ``**``, marks the
positional keyword variadic parameter for the
function: gathers all provided keyword arguments
left and closes the argslist. If present, this
must be the last parameter of the format list.
* A string defines a required parameter, accessible
positionally or through keyword
* A pair of ``[String, py.object]`` defines an
optional parameter and its default value.
For simplicity, when not using optional parameters
it is possible to use a simple string as the format
(using space-separated elements). The string will
be split on whitespace and processed as a normal
format array.
:returns: a javascript object mapping argument names to values
:raises: ``TypeError`` if the provided arguments don't match the
format
.. class:: py.PY_def(fn)
Type wrapping javascript functions into py.js callables. The
wrapped function follows :ref:`the py.js calling conventions
<types-methods-python-call>`
:param Function fn: the javascript function to wrap
:returns: a callable py.js object
.. [#kwonly] Python 2, which py.js currently implements, does not
support Python-level keyword-only parameters (it can be
done through the C-API), but it seemed neat and easy
enough so there.
.. [#star-args] due to this and contrary to Python 2, py.js allows
arguments other than ``**kwargs`` to follow ``*args``.
.. _PyArg_ParseTupleAndKeywords:
http://docs.python.org/c-api/arg.html#PyArg_ParseTupleAndKeywords

View File

@ -369,24 +369,26 @@ var py = {};
return py.False;
}
if (val instanceof py.object
|| val === py.object
|| py.issubclass.__call__([val, py.object]) === py.True) {
var fn = function () {}
fn.prototype = py.object;
if (py.PY_call(py.isinstance, [val, py.object]) === py.True
|| py.PY_call(py.issubclass, [val, py.object]) === py.True) {
return val;
}
switch (typeof val) {
case 'number':
return new py.float(val);
return py.float.fromJSON(val);
case 'string':
return new py.str(val);
return py.str.fromJSON(val);
case 'function':
return new py.def(val);
return py.PY_def.fromJSON(val);
}
switch(val.constructor) {
case Object:
var o = new py.object();
// TODO: why py.object instead of py.dict?
var o = py.PY_call(py.object);
for (var prop in val) {
if (val.hasOwnProperty(prop)) {
o[prop] = val[prop];
@ -394,40 +396,170 @@ var py = {};
}
return o;
case Array:
var a = new py.list();
a.values = val;
var a = py.PY_call(py.list);
a._values = val;
return a;
}
throw new Error("Could not convert " + val + " to a pyval");
}
// Builtins
py.type = function type(constructor, base, dict) {
var proto;
if (!base) {
base = py.object;
// JSAPI, JS-level utility functions for implementing new py.js
// types
py.py = {};
py.PY_parseArgs = function PY_parseArgs(argument, format) {
var out = {};
var args = argument[0];
var kwargs = {};
for (var k in argument[1]) {
kwargs[k] = argument[1][k];
}
proto = constructor.prototype = create(base.prototype);
proto.constructor = constructor;
if (dict) {
for(var k in dict) {
if (!dict.hasOwnProperty(k)) { continue; }
proto[k] = dict[k];
if (typeof format === 'string') {
format = format.split(/\s+/);
}
var name = function (spec) {
if (typeof spec === 'string') {
return spec;
} else if (spec instanceof Array && spec.length === 2) {
return spec[0];
}
throw new Error(
"TypeError: unknown format specification " +
JSON.stringify(spec));
};
// TODO: ensure all format arg names are actual names?
for(var i=0; i<args.length; ++i) {
var spec = format[i];
// spec list ended, or specs switching to keyword-only
if (!spec || spec === '*') {
throw new Error(
"TypeError: function takes exactly " + (i-1) +
" positional arguments (" + args.length +
" given")
} else if(/^\*\w/.test(spec)) {
// *args, final
out[name(spec.slice(1))] = args.slice(i);
break;
}
out[name(spec)] = args[i];
}
for(var j=i; j<format.length; ++j) {
var spec = format[j];
var n = name(spec);
if (n in out) {
throw new Error(
"TypeError: function got multiple values " +
"for keyword argument '" + kwarg + "'");
}
if (/^\*\*\w/.test(n)) {
// **kwarg
out[n.slice(2)] = kwargs;
kwargs = {};
break;
}
if (n in kwargs) {
out[n] = kwargs[n];
// Remove from args map
delete kwargs[n];
}
}
constructor.__call__ = function () {
// create equivalent type with same prototype
var instance = create(proto);
// call actual constructor
var res = constructor.apply(instance, arguments);
// return result of constructor if any, otherwise instance
return res || instance;
};
return constructor;
// Ensure all keyword arguments were consumed
for (var key in kwargs) {
throw new Error(
"TypeError: function got an unexpected keyword argument '"
+ key + "'");
}
// Fixup args count if there's a kwonly flag (or an *args)
var kwonly = 0;
for(var k = 0; k < format.length; ++k) {
if (/^\*/.test(format[k])) { kwonly = 1; break; }
}
// Check that all required arguments have been matched, add
// optional values
for(var k = 0; k < format.length; ++k) {
var spec = format[k], n = name(spec);
// kwonly, va_arg or matched argument
if (/^\*/.test(n) || n in out) { continue; }
// Unmatched required argument
if (!(spec instanceof Array)) {
throw new Error(
"TypeError: function takes exactly " + (format.length - kwonly)
+ " arguments");
}
// Set default value
out[n] = spec[1];
}
return out;
};
// Builtins
py.PY_call = function (callable, args, kwargs) {
if (!args) {
args = []; kwargs = {};
} else if (typeof args === 'object' && !(args instanceof Array)) {
kwargs = args;
args = [];
} else if (!kwargs) {
kwargs = {};
}
if (callable.__is_type) {
// class hack
var instance = callable.__new__.call(callable, args, kwargs);
var typ = function () {}
typ.prototype = callable;
if (instance instanceof typ) {
instance.__init__.call(instance, args, kwargs);
}
return instance
}
return callable.__call__(args, kwargs);
};
py.type = function type(name, bases, dict) {
var proto;
if (typeof name !== 'string') {
throw new Error("ValueError: a class name should be a string");
}
if (!bases || bases.length === 0) {
bases = [py.object];
} else if (bases.length > 1) {
throw new Error("ValueError: can't provide multiple bases for a "
+ "new type");
}
var base = bases[0];
var ClassObj = create(base);
if (dict) {
for (var k in dict) {
if (!dict.hasOwnProperty(k)) { continue; }
ClassObj[k] = dict[k];
}
}
ClassObj.__class__ = ClassObj;
ClassObj.__name__ = name;
ClassObj.__bases__ = bases;
ClassObj.__is_type = true;
return ClassObj;
};
py.type.__call__ = function () {
var args = py.PY_parseArgs(arguments, ['object']);
return args.object.__class__;
};
var hash_counter = 0;
py.object = py.type(function object() {}, {}, {
py.object = py.type('object', [{}], {
__new__: function () {
// If ``this`` isn't the class object, this is going to be
// beyond fucked up
var inst = create(this);
inst.__is_type = false;
return inst;
},
__init__: function () {},
// Basic customization
__hash__: function () {
if (this._hash) {
@ -466,11 +598,11 @@ var py = {};
var val = this[name];
if (typeof val === 'object' && '__get__' in val) {
// TODO: second argument should be class
return val.__get__(this);
return val.__get__(this, py.PY_call(py.type, [this]));
}
if (typeof val === 'function' && !this.hasOwnProperty(name)) {
// val is a method from the class
return new PY_instancemethod(val, this);
return PY_instancemethod.fromJSON(val, this);
}
return PY_ensurepy(val);
}
@ -492,140 +624,182 @@ var py = {};
throw new Error(this.constructor.name + ' can not be converted to JSON');
}
});
var NoneType = py.type(function NoneType() {}, py.object, {
var NoneType = py.type('NoneType', null, {
__nonzero__: function () { return py.False; },
toJSON: function () { return null; }
});
py.None = new NoneType();
var NotImplementedType = py.type(function NotImplementedType(){});
py.NotImplemented = new NotImplementedType();
py.None = py.PY_call(NoneType);
var NotImplementedType = py.type('NotImplementedType', null, {});
py.NotImplemented = py.PY_call(NotImplementedType);
var booleans_initialized = false;
py.bool = py.type(function bool(value) {
value = (value instanceof Array) ? value[0] : value;
// The only actual instance of py.bool should be py.True
// and py.False. Return the new instance of py.bool if we
// are initializing py.True and py.False, otherwise always
// return either py.True or py.False.
if (!booleans_initialized) {
return;
}
if (value === undefined) { return py.False; }
return value.__nonzero__() === py.True ? py.True : py.False;
}, py.object, {
py.bool = py.type('bool', null, {
__new__: function () {
if (!booleans_initialized) {
return py.object.__new__.apply(this);
}
var ph = {};
var args = py.PY_parseArgs(arguments, [['value', ph]]);
if (args.value === ph) {
return py.False;
}
return args.value.__nonzero__() === py.True ? py.True : py.False;
},
__nonzero__: function () { return this; },
toJSON: function () { return this === py.True; }
});
py.True = new py.bool();
py.False = new py.bool();
py.True = py.PY_call(py.bool);
py.False = py.PY_call(py.bool);
booleans_initialized = true;
py.float = py.type(function float(value) {
value = (value instanceof Array) ? value[0] : value;
if (value === undefined) { this._value = 0; return; }
if (value instanceof py.float) { return value; }
if (typeof value === 'number' || value instanceof Number) {
this._value = value;
return;
}
if (typeof value === 'string' || value instanceof String) {
this._value = parseFloat(value);
return;
}
if (value instanceof py.object && '__float__' in value) {
var res = value.__float__();
if (res instanceof py.float) {
return res;
py.float = py.type('float', null, {
__init__: function () {
var placeholder = {};
var args = py.PY_parseArgs(arguments, [['value', placeholder]]);
var value = args.value;
if (value === placeholder) {
this._value = 0; return;
}
throw new Error('TypeError: __float__ returned non-float (type ' +
res.constructor.name + ')');
}
throw new Error('TypeError: float() argument must be a string or a number');
}, py.object, {
if (py.PY_call(py.isinstance, [value, py.float]) === py.True) {
this._value = value._value;
}
if (py.PY_call(py.isinstance, [value, py.object]) === py.True
&& '__float__' in value) {
var res = value.__float__();
if (py.PY_call(py.isinstance, [res, py.float]) === py.True) {
this._value = res._value;
return;
}
throw new Error('TypeError: __float__ returned non-float (type ' +
res.__class__.__name__ + ')');
}
throw new Error('TypeError: float() argument must be a string or a number');
},
__eq__: function (other) {
return this._value === other._value ? py.True : py.False;
},
__lt__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return this._value < other._value ? py.True : py.False;
},
__le__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return this._value <= other._value ? py.True : py.False;
},
__gt__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return this._value > other._value ? py.True : py.False;
},
__ge__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return this._value >= other._value ? py.True : py.False;
},
__add__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value + other._value);
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value + other._value);
},
__neg__: function () {
return new py.float(-this._value);
return py.float.fromJSON(-this._value);
},
__sub__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value - other._value);
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value - other._value);
},
__mul__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value * other._value);
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value * other._value);
},
__div__: function (other) {
if (!(other instanceof py.float)) { return py.NotImplemented; }
return new py.float(this._value / other._value);
if (py.PY_call(py.isinstance, [other, py.float]) !== py.True) {
return py.NotImplemented;
}
return py.float.fromJSON(this._value / other._value);
},
__nonzero__: function () {
return this._value ? py.True : py.False;
},
fromJSON: function (v) {
if (!(typeof v === 'number')) {
throw new Error('py.float.fromJSON can only take numbers ');
}
var instance = py.PY_call(py.float);
instance._value = v;
return instance;
},
toJSON: function () {
return this._value;
}
});
py.str = py.type(function str(s) {
s = (s instanceof Array) ? s[0] : s;
if (s === undefined) { this._value = ''; return; }
if (s instanceof py.str) { return s; }
if (typeof s === 'string' || s instanceof String) {
this._value = s;
return;
}
var v = s.__str__();
if (v instanceof py.str) { return v; }
throw new Error('TypeError: __str__ returned non-string (type ' +
v.constructor.name + ')');
}, py.object, {
py.str = py.type('str', null, {
__init__: function () {
var placeholder = {};
var args = py.PY_parseArgs(arguments, [['value', placeholder]]);
var s = args.value;
if (s === placeholder) { this._value = ''; return; }
if (py.PY_call(py.isinstance, [s, py.str]) === py.True) {
this._value = s._value;
return;
}
var v = s.__str__();
if (py.PY_call(py.isinstance, [v, py.str]) === py.True) {
this._value = v._value;
return;
}
throw new Error('TypeError: __str__ returned non-string (type ' +
v.__class__.__name__ + ')');
},
__hash__: function () {
return '\1\0\1' + this._value;
},
__eq__: function (other) {
if (other instanceof py.str && this._value === other._value) {
if (py.PY_call(py.isinstance, [other, py.str]) === py.True
&& this._value === other._value) {
return py.True;
}
return py.False;
},
__lt__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) {
return py.NotImplemented;
}
return this._value < other._value ? py.True : py.False;
},
__le__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) {
return py.NotImplemented;
}
return this._value <= other._value ? py.True : py.False;
},
__gt__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) {
return py.NotImplemented;
}
return this._value > other._value ? py.True : py.False;
},
__ge__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) {
return py.NotImplemented;
}
return this._value >= other._value ? py.True : py.False;
},
__add__: function (other) {
if (!(other instanceof py.str)) { return py.NotImplemented; }
return new py.str(this._value + other._value);
if (py.PY_call(py.isinstance, [other, py.str]) !== py.True) {
return py.NotImplemented;
}
return py.str.fromJSON(this._value + other._value);
},
__nonzero__: function () {
return this._value.length ? py.True : py.False;
@ -633,40 +807,46 @@ var py = {};
__contains__: function (s) {
return (this._value.indexOf(s._value) !== -1) ? py.True : py.False;
},
fromJSON: function (s) {
if (typeof s === 'string') {
var instance = py.PY_call(py.str);
instance._value = s;
return instance;
};
throw new Error("str.fromJSON can only take strings");
},
toJSON: function () {
return this._value;
}
});
py.tuple = py.type(function tuple() {}, null, {
py.tuple = py.type('tuple', null, {
__init__: function () {
this._values = [];
},
__contains__: function (value) {
for(var i=0, len=this.values.length; i<len; ++i) {
if (this.values[i].__eq__(value) === py.True) {
for(var i=0, len=this._values.length; i<len; ++i) {
if (this._values[i].__eq__(value) === py.True) {
return py.True;
}
}
return py.False;
},
__getitem__: function (index) {
return PY_ensurepy(this.values[index.toJSON()]);
return PY_ensurepy(this._values[index.toJSON()]);
},
toJSON: function () {
var out = [];
for (var i=0; i<this.values.length; ++i) {
out.push(this.values[i].toJSON());
for (var i=0; i<this._values.length; ++i) {
out.push(this._values[i].toJSON());
}
return out;
}
});
py.list = py.tuple;
py.dict = py.type(function dict(d) {
this._store = {};
for (var k in (d || {})) {
if (!d.hasOwnProperty(k)) { continue; }
var py_k = new py.str(k);
var val = PY_ensurepy(d[k]);
this._store[py_k.__hash__()] = [py_k, val];
}
}, py.object, {
py.dict = py.type('dict', null, {
__init__: function () {
this._store = {};
},
__getitem__: function (key) {
var h = key.__hash__();
if (!(h in this._store)) {
@ -677,14 +857,24 @@ var py = {};
__setitem__: function (key, value) {
this._store[key.__hash__()] = [key, value];
},
get: function (args) {
var h = args[0].__hash__();
var def = args.length > 1 ? args[1] : py.None;
get: function () {
var args = py.PY_parseArgs(arguments, ['k', ['d', py.None]]);
var h = args.k.__hash__();
if (!(h in this._store)) {
return def;
return args.d;
}
return this._store[h][1];
},
fromJSON: function (d) {
var instance = py.PY_call(py.dict);
for (var k in (d || {})) {
if (!d.hasOwnProperty(k)) { continue; }
instance.__setitem__(
py.str.fromJSON(k),
PY_ensurepy(d[k]));
}
return instance;
},
toJSON: function () {
var out = {};
for(var k in this._store) {
@ -694,30 +884,57 @@ var py = {};
return out;
}
});
py.def = py.type(function def(nativefunc) {
this._inst = null;
this._func = nativefunc;
}, py.object, {
__call__: function (args, kwargs) {
py.PY_def = py.type('function', null, {
__call__: function () {
// don't want to rewrite __call__ for instancemethod
return this._func.call(this._inst, args, kwargs);
return this._func.apply(this._inst, arguments);
},
fromJSON: function (nativefunc) {
var instance = py.PY_call(py.PY_def);
instance._inst = null;
instance._func = nativefunc;
return instance;
},
toJSON: function () {
return this._func;
}
});
var PY_instancemethod = py.type(function instancemethod(nativefunc, instance, _cls) {
// could also use bind?
this._inst = instance;
this._func = nativefunc;
}, py.def, {});
py.classmethod = py.type('classmethod', null, {
__init__: function () {
var args = py.PY_parseArgs(arguments, 'function');
this._func = args['function'];
},
__get__: function (obj, type) {
return PY_instancemethod.fromJSON(this._func, type);
},
fromJSON: function (func) {
return py.PY_call(py.classmethod, [func]);
}
});
var PY_instancemethod = py.type('instancemethod', [py.PY_def], {
fromJSON: function (nativefunc, instance) {
var inst = py.PY_call(PY_instancemethod);
// could also use bind?
inst._inst = instance;
inst._func = nativefunc;
return inst;
}
});
py.issubclass = new py.def(function issubclass(args) {
var derived = args[0], parent = args[1];
// still hurts my brain that this can work
return derived.prototype instanceof py.object
? py.True
: py.False;
py.isinstance = new py.PY_def.fromJSON(function isinstance() {
var args = py.PY_parseArgs(arguments, ['object', 'class']);
var fn = function () {};
fn.prototype = args['class'];
return (args.object instanceof fn) ? py.True : py.False;
});
py.issubclass = new py.PY_def.fromJSON(function issubclass() {
var args = py.PY_parseArgs(arguments, ['C', 'B']);
var fn = function () {};
fn.prototype = args.B;
if (args.C === args.B || args.C instanceof fn) {
return py.True;
}
return py.False;
});
@ -726,10 +943,10 @@ var py = {};
'==': ['eq', 'eq', function (a, b) { return a === b; }],
'!=': ['ne', 'ne', function (a, b) { return a !== b; }],
'<>': ['ne', 'ne', function (a, b) { return a !== b; }],
'<': ['lt', 'gt', function (a, b) {return a.constructor.name < b.constructor.name;}],
'<=': ['le', 'ge', function (a, b) {return a.constructor.name <= b.constructor.name;}],
'>': ['gt', 'lt', function (a, b) {return a.constructor.name > b.constructor.name;}],
'>=': ['ge', 'le', function (a, b) {return a.constructor.name >= b.constructor.name;}],
'<': ['lt', 'gt', function (a, b) {return a.__class__.__name__ < b.__class__.__name__;}],
'<=': ['le', 'ge', function (a, b) {return a.__class__.__name__ <= b.__class__.__name__;}],
'>': ['gt', 'lt', function (a, b) {return a.__class__.__name__ > b.__class__.__name__;}],
'>=': ['ge', 'le', function (a, b) {return a.__class__.__name__ >= b.__class__.__name__;}],
'+': ['add', 'radd'],
'-': ['sub', 'rsub'],
@ -772,8 +989,8 @@ var py = {};
}
throw new Error(
"TypeError: unsupported operand type(s) for " + op + ": '"
+ o1.constructor.name + "' and '"
+ o2.constructor.name + "'");
+ o1.__class__.__name__ + "' and '"
+ o2.__class__.__name__ + "'");
};
var PY_builtins = {
@ -787,10 +1004,15 @@ var py = {};
object: py.object,
bool: py.bool,
float: py.float,
str: py.str,
unicode: py.unicode,
tuple: py.tuple,
list: py.list,
dict: py.dict,
issubclass: py.issubclass
isinstance: py.isinstance,
issubclass: py.issubclass,
classmethod: py.classmethod,
};
py.parse = function (toks) {
@ -825,9 +1047,9 @@ var py = {};
}
return PY_ensurepy(val, expr.value);
case '(string)':
return new py.str(expr.value);
return py.str.fromJSON(expr.value);
case '(number)':
return new py.float(expr.value);
return py.float.fromJSON(expr.value);
case '(constant)':
switch (expr.value) {
case 'None': return py.None;
@ -876,7 +1098,7 @@ var py = {};
py.evaluate(arg.second, context);
}
}
return callable.__call__(args, kwargs);
return py.PY_call(callable, args, kwargs);
}
var tuple_exprs = expr.first,
tuple_values = [];
@ -884,8 +1106,8 @@ var py = {};
tuple_values.push(py.evaluate(
tuple_exprs[j], context));
}
var t = new py.tuple();
t.values = tuple_values;
var t = py.PY_call(py.tuple);
t._values = tuple_values;
return t;
case '[':
if (expr.second) {
@ -897,11 +1119,11 @@ var py = {};
list_values.push(py.evaluate(
list_exprs[k], context));
}
var l = new py.list();
l.values = list_values;
var l = py.PY_call(py.list);
l._values = list_values;
return l;
case '{':
var dict_exprs = expr.first, dict = new py.dict;
var dict_exprs = expr.first, dict = py.PY_call(py.dict);
for(var l=0; l<dict_exprs.length; ++l) {
dict.__setitem__(
py.evaluate(dict_exprs[l][0], context),

View File

@ -1,132 +0,0 @@
var py = require('../lib/py.js'),
expect = require('expect.js');
expect.Assertion.prototype.tokens = function (n) {
var length = this.obj.length;
this.assert(length === n + 1,
'expected ' + this.obj + ' to have ' + n + ' tokens',
'expected ' + this.obj + ' to not have ' + n + ' tokens');
this.assert(this.obj[length-1].id === '(end)',
'expected ' + this.obj + ' to have and end token',
'expected ' + this.obj + ' to not have an end token');
};
expect.Assertion.prototype.named = function (value) {
this.assert(this.obj.id === '(name)',
'expected ' + this.obj + ' to be a name token',
'expected ' + this.obj + ' not to be a name token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
expect.Assertion.prototype.constant = function (value) {
this.assert(this.obj.id === '(constant)',
'expected ' + this.obj + ' to be a constant token',
'expected ' + this.obj + ' not to be a constant token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
expect.Assertion.prototype.number = function (value) {
this.assert(this.obj.id === '(number)',
'expected ' + this.obj + ' to be a number token',
'expected ' + this.obj + ' not to be a number token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
expect.Assertion.prototype.string = function (value) {
this.assert(this.obj.id === '(string)',
'expected ' + this.obj + ' to be a string token',
'expected ' + this.obj + ' not to be a string token');
this.assert(this.obj.value === value,
'expected ' + this.obj + ' to have tokenized ' + value,
'expected ' + this.obj + ' not to have tokenized ' + value);
};
describe('Tokenizer', function () {
describe('simple literals', function () {
it('tokenizes numbers', function () {
var toks = py.tokenize('1');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.number(1);
var toks = py.tokenize('-1');
expect(toks).to.have.tokens(2);
expect(toks[0].id).to.be('-');
expect(toks[1]).to.be.number(1);
var toks = py.tokenize('1.2');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.number(1.2);
var toks = py.tokenize('.42');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.number(0.42);
});
it('tokenizes strings', function () {
var toks = py.tokenize('"foo"');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.string('foo');
var toks = py.tokenize("'foo'");
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.string('foo');
});
it('tokenizes bare names', function () {
var toks = py.tokenize('foo');
expect(toks).to.have.tokens(1);
expect(toks[0].id).to.be('(name)');
expect(toks[0].value).to.be('foo');
});
it('tokenizes constants', function () {
var toks = py.tokenize('None');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('None');
var toks = py.tokenize('True');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('True');
var toks = py.tokenize('False');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('False');
});
it('does not fuck up on trailing spaces', function () {
var toks = py.tokenize('None ');
expect(toks).to.have.tokens(1);
expect(toks[0]).to.be.constant('None');
});
});
describe('collections', function () {
it('tokenizes opening and closing symbols', function () {
var toks = py.tokenize('()');
expect(toks).to.have.tokens(2);
expect(toks[0].id).to.be('(');
expect(toks[1].id).to.be(')');
});
});
describe('functions', function () {
it('tokenizes kwargs', function () {
var toks = py.tokenize('foo(bar=3, qux=4)');
expect(toks).to.have.tokens(10);
});
});
});
describe('Parser', function () {
describe('functions', function () {
var ast = py.parse(py.tokenize('foo(bar=3, qux=4)'));
expect(ast.id).to.be('(');
expect(ast.first).to.be.named('foo');
args = ast.second;
expect(args[0].id).to.be('=');
expect(args[0].first).to.be.named('bar');
expect(args[0].second).to.be.number(3);
expect(args[1].id).to.be('=');
expect(args[1].first).to.be.named('qux');
expect(args[1].second).to.be.number(4);
});
});

View File

@ -1,416 +0,0 @@
var py = require('../lib/py.js'),
expect = require('expect.js');
var ev = function (str, context) {
return py.evaluate(py.parse(py.tokenize(str)), context);
};
describe('Literals', function () {
describe('Number', function () {
it('should have the right type', function () {
expect(ev('1')).to.be.a(py.float);
});
it('should yield the corresponding JS value', function () {
expect(py.eval('1')).to.be(1);
expect(py.eval('42')).to.be(42);
expect(py.eval('9999')).to.be(9999);
});
it('should correctly handle negative literals', function () {
expect(py.eval('-1')).to.be(-1);
expect(py.eval('-42')).to.be(-42);
expect(py.eval('-9999')).to.be(-9999);
});
it('should correctly handle float literals', function () {
expect(py.eval('.42')).to.be(0.42);
expect(py.eval('1.2')).to.be(1.2);
});
});
describe('Booleans', function () {
it('should have the right type', function () {
expect(ev('False')).to.be.a(py.bool);
expect(ev('True')).to.be.a(py.bool);
});
it('should yield the corresponding JS value', function () {
expect(py.eval('False')).to.be(false);
expect(py.eval('True')).to.be(true);
});
});
describe('None', function () {
it('should have the right type', function () {
expect(ev('None')).to.be.a(py.object)
});
it('should yield a JS null', function () {
expect(py.eval('None')).to.be(null);
});
});
describe('String', function () {
it('should have the right type', function () {
expect(ev('"foo"')).to.be.a(py.str);
expect(ev("'foo'")).to.be.a(py.str);
});
it('should yield the corresponding JS string', function () {
expect(py.eval('"somestring"')).to.be('somestring');
expect(py.eval("'somestring'")).to.be('somestring');
});
});
describe('Tuple', function () {
it('shoud have the right type', function () {
expect(ev('()')).to.be.a(py.tuple);
});
it('should map to a JS array', function () {
expect(py.eval('()')).to.eql([]);
expect(py.eval('(1, 2, 3)')).to.eql([1, 2, 3]);
});
});
describe('List', function () {
it('shoud have the right type', function () {
expect(ev('[]')).to.be.a(py.list);
});
it('should map to a JS array', function () {
expect(py.eval('[]')).to.eql([]);
expect(py.eval('[1, 2, 3]')).to.eql([1, 2, 3]);
});
});
describe('Dict', function () {
it('shoud have the right type', function () {
expect(ev('{}')).to.be.a(py.dict);
});
it('should map to a JS object', function () {
expect(py.eval("{}")).to.eql({});
expect(py.eval("{'foo': 1, 'bar': 2}"))
.to.eql({foo: 1, bar: 2});
});
});
});
describe('Free variables', function () {
it('should return its identity', function () {
expect(py.eval('foo', {foo: 1})).to.be(1);
expect(py.eval('foo', {foo: true})).to.be(true);
expect(py.eval('foo', {foo: false})).to.be(false);
expect(py.eval('foo', {foo: null})).to.be(null);
expect(py.eval('foo', {foo: 'bar'})).to.be('bar');
});
});
describe('Comparisons', function () {
describe('equality', function () {
it('should work with literals', function () {
expect(py.eval('1 == 1')).to.be(true);
expect(py.eval('"foo" == "foo"')).to.be(true);
expect(py.eval('"foo" == "bar"')).to.be(false);
});
it('should work with free variables', function () {
expect(py.eval('1 == a', {a: 1})).to.be(true);
expect(py.eval('foo == "bar"', {foo: 'bar'})).to.be(true);
expect(py.eval('foo == "bar"', {foo: 'qux'})).to.be(false);
});
});
describe('inequality', function () {
it('should work with literals', function () {
expect(py.eval('1 != 2')).to.be(true);
expect(py.eval('"foo" != "foo"')).to.be(false);
expect(py.eval('"foo" != "bar"')).to.be(true);
});
it('should work with free variables', function () {
expect(py.eval('1 != a', {a: 42})).to.be(true);
expect(py.eval('foo != "bar"', {foo: 'bar'})).to.be(false);
expect(py.eval('foo != "bar"', {foo: 'qux'})).to.be(true);
expect(py.eval('foo != bar', {foo: 'qux', bar: 'quux'}))
.to.be(true);
});
it('should accept deprecated form', function () {
expect(py.eval('1 <> 2')).to.be(true);
expect(py.eval('"foo" <> "foo"')).to.be(false);
expect(py.eval('"foo" <> "bar"')).to.be(true);
});
});
describe('rich comparisons', function () {
it('should work with numbers', function () {
expect(py.eval('3 < 5')).to.be(true);
expect(py.eval('5 >= 3')).to.be(true);
expect(py.eval('3 >= 3')).to.be(true);
expect(py.eval('3 > 5')).to.be(false);
});
it('should support comparison chains', function () {
expect(py.eval('1 < 3 < 5')).to.be(true);
expect(py.eval('5 > 3 > 1')).to.be(true);
expect(py.eval('1 < 3 > 2 == 2 > -2')).to.be(true);
});
it('should compare strings', function () {
expect(py.eval('date >= current',
{date: '2010-06-08', current: '2010-06-05'}))
.to.be(true);
expect(py.eval('state == "cancel"', {state: 'cancel'}))
.to.be(true);
expect(py.eval('state == "cancel"', {state: 'open'}))
.to.be(false);
});
});
describe('missing eq/neq', function () {
it('should fall back on identity', function () {
var typ = new py.type(function MyType() {});
expect(py.eval('MyType() == MyType()', {MyType: typ})).to.be(false);
});
});
describe('un-comparable types', function () {
it('should default to type-name ordering', function () {
var t1 = new py.type(function Type1() {});
var t2 = new py.type(function Type2() {});
expect(py.eval('T1() < T2()', {T1: t1, T2: t2})).to.be(true);
expect(py.eval('T1() > T2()', {T1: t1, T2: t2})).to.be(false);
});
it('should handle native stuff', function () {
expect(py.eval('None < 42')).to.be(true);
expect(py.eval('42 > None')).to.be(true);
expect(py.eval('None > 42')).to.be(false);
expect(py.eval('None < False')).to.be(true);
expect(py.eval('None < True')).to.be(true);
expect(py.eval('False > None')).to.be(true);
expect(py.eval('True > None')).to.be(true);
expect(py.eval('None > False')).to.be(false);
expect(py.eval('None > True')).to.be(false);
expect(py.eval('False < ""')).to.be(true);
expect(py.eval('"" > False')).to.be(true);
expect(py.eval('False > ""')).to.be(false);
});
});
});
describe('Boolean operators', function () {
it('should work', function () {
expect(py.eval("foo == 'foo' or foo == 'bar'",
{foo: 'bar'}))
.to.be(true);;
expect(py.eval("foo == 'foo' and bar == 'bar'",
{foo: 'foo', bar: 'bar'}))
.to.be(true);;
});
it('should be lazy', function () {
// second clause should nameerror if evaluated
expect(py.eval("foo == 'foo' or bar == 'bar'",
{foo: 'foo'}))
.to.be(true);;
expect(py.eval("foo == 'foo' and bar == 'bar'",
{foo: 'bar'}))
.to.be(false);;
});
it('should return the actual object', function () {
expect(py.eval('"foo" or "bar"')).to.be('foo');
expect(py.eval('None or "bar"')).to.be('bar');
expect(py.eval('False or None')).to.be(null);
expect(py.eval('0 or 1')).to.be(1);
});
});
describe('Containment', function () {
describe('in sequences', function () {
it('should match collection items', function () {
expect(py.eval("'bar' in ('foo', 'bar')"))
.to.be(true);
expect(py.eval('1 in (1, 2, 3, 4)'))
.to.be(true);;
expect(py.eval('1 in (2, 3, 4)'))
.to.be(false);;
expect(py.eval('"url" in ("url",)'))
.to.be(true);
expect(py.eval('"foo" in ["foo", "bar"]'))
.to.be(true);
});
it('should not be recursive', function () {
expect(py.eval('"ur" in ("url",)'))
.to.be(false);;
});
it('should be negatable', function () {
expect(py.eval('1 not in (2, 3, 4)')).to.be(true);
expect(py.eval('"ur" not in ("url",)')).to.be(true);
expect(py.eval('-2 not in (1, 2, 3)')).to.be(true);
});
});
describe('in dict', function () {
// TODO
});
describe('in strings', function () {
it('should match the whole string', function () {
expect(py.eval('"view" in "view"')).to.be(true);
expect(py.eval('"bob" in "view"')).to.be(false);
});
it('should match substrings', function () {
expect(py.eval('"ur" in "url"')).to.be(true);
});
});
});
describe('Conversions', function () {
describe('to bool', function () {
describe('strings', function () {
it('should be true if non-empty', function () {
expect(py.eval('bool(date_deadline)',
{date_deadline: '2008'}))
.to.be(true);
});
it('should be false if empty', function () {
expect(py.eval('bool(s)', {s: ''})) .to.be(false);
});
});
});
});
describe('Attribute access', function () {
it("should return the attribute's value", function () {
var o = new py.object();
o.bar = py.True;
expect(py.eval('foo.bar', {foo: o})).to.be(true);
o.bar = py.False;
expect(py.eval('foo.bar', {foo: o})).to.be(false);
});
it("should work with functions", function () {
var o = new py.object();
o.bar = new py.def(function () {
return new py.str("ok");
});
expect(py.eval('foo.bar()', {foo: o})).to.be('ok');
});
it('should not convert function attributes into methods', function () {
var o = new py.object();
o.bar = new py.type(function bar() {});
o.bar.__getattribute__ = function () {
return o.bar.baz;
}
o.bar.baz = py.True;
expect(py.eval('foo.bar.baz', {foo: o})).to.be(true);
});
it('should work on instance attributes', function () {
var typ = py.type(function MyType() {
this.attr = new py.float(3);
}, py.object, {});
expect(py.eval('MyType().attr', {MyType: typ})).to.be(3);
});
it('should work on class attributes', function () {
var typ = py.type(function MyType() {}, py.object, {
attr: new py.float(3)
});
expect(py.eval('MyType().attr', {MyType: typ})).to.be(3);
});
it('should work with methods', function () {
var typ = py.type(function MyType() {
this.attr = new py.float(3);
}, py.object, {
some_method: function () { return new py.str('ok'); },
get_attr: function () { return this.attr; }
});
expect(py.eval('MyType().some_method()', {MyType: typ})).to.be('ok');
expect(py.eval('MyType().get_attr()', {MyType: typ})).to.be(3);
});
});
describe('Callables', function () {
it('should wrap JS functions', function () {
expect(py.eval('foo()', {foo: function foo() { return new py.float(3); }}))
.to.be(3);
});
it('should work on custom types', function () {
var typ = py.type(function MyType() {}, py.object, {
toJSON: function () { return true; }
});
expect(py.eval('MyType()', {MyType: typ})).to.be(true);
});
it('should accept kwargs', function () {
expect(py.eval('foo(ok=True)', {
foo: function foo() { return py.True; }
})).to.be(true);
});
it('should be able to get its kwargs', function () {
expect(py.eval('foo(ok=True)', {
foo: function foo(args, kwargs) { return kwargs.ok; }
})).to.be(true);
});
it('should be able to have both args and kwargs', function () {
expect(py.eval('foo(1, 2, 3, ok=True, nok=False)', {
foo: function (args, kwargs) {
expect(args).to.have.length(3);
expect(args[0].toJSON()).to.be(1);
expect(kwargs).to.only.have.keys('ok', 'nok')
expect(kwargs.nok.toJSON()).to.be(false);
return kwargs.ok;
}
})).to.be(true);
});
});
describe('issubclass', function () {
it('should say a type is its own subclass', function () {
expect(py.issubclass.__call__([py.dict, py.dict]).toJSON())
.to.be(true);
expect(py.eval('issubclass(dict, dict)'))
.to.be(true);
});
it('should work with subtypes', function () {
expect(py.issubclass.__call__([py.bool, py.object]).toJSON())
.to.be(true);
});
});
describe('builtins', function () {
it('should aways be available', function () {
expect(py.eval('bool("foo")')).to.be(true);
});
});
describe('numerical protocols', function () {
describe('True numbers (float)', function () {
describe('Basic arithmetic', function () {
it('can be added', function () {
expect(py.eval('1 + 1')).to.be(2);
expect(py.eval('1.5 + 2')).to.be(3.5);
expect(py.eval('1 + -1')).to.be(0);
});
it('can be subtracted', function () {
expect(py.eval('1 - 1')).to.be(0);
expect(py.eval('1.5 - 2')).to.be(-0.5);
expect(py.eval('2 - 1.5')).to.be(0.5);
});
it('can be multiplied', function () {
expect(py.eval('1 * 3')).to.be(3);
expect(py.eval('0 * 5')).to.be(0);
expect(py.eval('42 * -2')).to.be(-84);
});
it('can be divided', function () {
expect(py.eval('1 / 2')).to.be(0.5);
expect(py.eval('2 / 1')).to.be(2);
});
});
});
describe('Strings', function () {
describe('Basic arithmetics operators', function () {
it('can be added (concatenation)', function () {
expect(py.eval('"foo" + "bar"')).to.be('foobar');
});
});
});
});
describe('dicts', function () {
it('should be possible to retrieve their value', function () {
var d = new py.dict({foo: 3, bar: 4, baz: 5});
expect(py.eval('d["foo"]', {d: d})).to.be(3);
expect(py.eval('d["baz"]', {d: d})).to.be(5);
});
it('should raise KeyError if a key is missing', function () {
var d = new py.dict();
expect(function () {
py.eval('d["foo"]', {d: d});
}).to.throwException(/^KeyError/);
});
it('should have a method to provide a default value', function () {
var d = new py.dict({foo: 3});
expect(py.eval('d.get("foo")', {d: d})).to.be(3);
expect(py.eval('d.get("bar")', {d: d})).to.be(null);
expect(py.eval('d.get("bar", 42)', {d: d})).to.be(42);
var e = new py.dict({foo: null});
expect(py.eval('d.get("foo")', {d: e})).to.be(null);
expect(py.eval('d.get("bar")', {d: e})).to.be(null);
});
});
describe('Type converter', function () {
it('should convert bare objects to objects', function () {
expect(py.eval('foo.bar', {foo: {bar: 3}})).to.be(3);
});
it('should convert arrays to lists', function () {
expect(py.eval('foo[3]', {foo: [9, 8, 7, 6, 5]}))
.to.be(6);
});
});

View File

@ -4,66 +4,151 @@
openerp.web.pyeval = function (instance) {
instance.web.pyeval = {};
var obj = function () {};
obj.prototype = py.object;
var asJS = function (arg) {
if (arg instanceof py.object) {
if (arg instanceof obj) {
return arg.toJSON();
}
return arg;
};
var datetime = new py.object();
datetime.datetime = new py.type(function datetime() {
throw new Error('datetime.datetime not implemented');
var datetime = py.PY_call(py.object);
datetime.datetime = py.type('datetime', null, {
__init__: function () {
var zero = py.float.fromJSON(0);
var args = py.PY_parseArgs(arguments, [
'year', 'month', 'day',
['hour', zero], ['minute', zero], ['second', zero],
['microsecond', zero], ['tzinfo', py.None]
]);
for(var key in args) {
if (!args.hasOwnProperty(key)) { continue; }
this[key] = asJS(args[key]);
}
},
strftime: function () {
var self = this;
var args = py.PY_parseArgs(arguments, 'format');
return py.str.fromJSON(args.format.toJSON()
.replace(/%([A-Za-z])/g, function (m, c) {
switch (c) {
case 'Y': return self.year;
case 'm': return _.str.sprintf('%02d', self.month);
case 'd': return _.str.sprintf('%02d', self.day);
case 'H': return _.str.sprintf('%02d', self.hour);
case 'M': return _.str.sprintf('%02d', self.minute);
case 'S': return _.str.sprintf('%02d', self.second);
}
throw new Error('ValueError: No known conversion for ' + m);
}));
},
now: py.classmethod.fromJSON(function () {
var d = new Date();
return py.PY_call(datetime.datetime,
[d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(),
d.getUTCMilliseconds() * 1000]);
}),
today: py.classmethod.fromJSON(function () {
var d = new Date();
return py.PY_call(datetime.datetime,
[d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]);
}),
combine: py.classmethod.fromJSON(function () {
var args = py.PY_parseArgs(arguments, 'date time');
return py.PY_call(datetime.datetime, [
// FIXME: should use getattr
args.date.year,
args.date.month,
args.date.day,
args.time.hour,
args.time.minute,
args.time.second
]);
})
});
var date = datetime.date = new py.type(function date(y, m, d) {
if (y instanceof Array) {
d = y[2];
m = y[1];
y = y[0];
}
this.year = asJS(y);
this.month = asJS(m);
this.day = asJS(d);
}, py.object, {
strftime: function (args) {
var f = asJS(args[0]), self = this;
return new py.str(f.replace(/%([A-Za-z])/g, function (m, c) {
switch (c) {
case 'Y': return self.year;
case 'm': return _.str.sprintf('%02d', self.month);
case 'd': return _.str.sprintf('%02d', self.day);
}
throw new Error('ValueError: No known conversion for ' + m);
}));
}
datetime.date = py.type('date', null, {
__init__: function () {
var args = py.PY_parseArgs(arguments, 'year month day');
this.year = asJS(args.year);
this.month = asJS(args.month);
this.day = asJS(args.day);
},
strftime: function () {
var self = this;
var args = py.PY_parseArgs(arguments, 'format');
return py.str.fromJSON(args.format.toJSON()
.replace(/%([A-Za-z])/g, function (m, c) {
switch (c) {
case 'Y': return self.year;
case 'm': return _.str.sprintf('%02d', self.month);
case 'd': return _.str.sprintf('%02d', self.day);
}
throw new Error('ValueError: No known conversion for ' + m);
}));
},
today: py.classmethod.fromJSON(function () {
var d = new Date();
return py.PY_call(
datetime.date, [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()]);
})
});
date.__getattribute__ = function (name) {
if (name === 'today') {
return date.today;
datetime.time = py.type('time', null, {
__init__: function () {
var zero = py.float.fromJSON(0);
var args = py.PY_parseArgs(arguments, [
['hour', zero], ['minute', zero], ['second', zero], ['microsecond', zero],
['tzinfo', py.None]
]);
for(var k in args) {
if (!args.hasOwnProperty(k)) { continue; }
this[k] = asJS(args[k]);
}
}
throw new Error("AttributeError: object 'date' has no attribute '" + name +"'");
};
date.today = new py.def(function () {
var d = new Date();
return new date(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate());
});
datetime.time = new py.type(function time() {
throw new Error('datetime.time not implemented');
});
var time = new py.object();
time.strftime = new py.def(function (args) {
return date.today.__call__().strftime(args);
datetime.timedelta = py.type('timedelta', null, {
__init__: function () {
var zero = py.float.fromJSON(0);
var args = py.PY_parseArgs(arguments,
[['days', zero], ['seconds', zero], ['microseconds', zero],
['milliseconds', zero], ['minutes', zero], ['hours', zero],
['weeks', zero]]);
var ms = args['microseconds'].toJSON()
+ 1000 * args['milliseconds'].toJSON();
this.milliseconds = ms % 1000000;
var sec = Math.floor(ms / 1000000)
+ args['seconds'].toJSON()
+ 60 * args['minutes'].toJSON()
+ 3600 * args['hours'].toJSON();
this.seconds = sec % 86400;
this.days = Math.floor(sec / 86400)
+ args['days'].toJSON()
+ 7 * args['weeks'].toJSON();
}
});
var relativedelta = new py.type(function relativedelta(args, kwargs) {
if (!_.isEmpty(args)) {
throw new Error('Extraction of relative deltas from existing datetimes not supported');
}
this.ops = kwargs;
}, py.object, {
var time = py.PY_call(py.object);
time.strftime = py.PY_def.fromJSON(function () {
// FIXME: needs PY_getattr
var d = py.PY_call(datetime.__getattribute__('datetime')
.__getattribute__('now'));
var args = [].slice.call(arguments);
return py.PY_call.apply(
null, [d.__getattribute__('strftime')].concat(args));
});
var relativedelta = py.type('relativedelta', null, {
__init__: function () {
this.ops = py.PY_parseArgs(arguments,
'* year month day hour minute second microsecond '
+ 'years months weeks days hours minutes secondes microseconds '
+ 'weekday leakdays yearday nlyearday');
},
__add__: function (other) {
if (!(other instanceof datetime.date)) {
if (py.PY_call(py.isinstance, [datetime.date]) !== py.True) {
return py.NotImplemented;
}
// TODO: test this whole mess
@ -96,14 +181,19 @@ openerp.web.pyeval = function (instance) {
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
return new datetime.date(year, month, day);
// FIXME: use date.replace
return py.PY_call(datetime.date, [
py.float.fromJSON(year),
py.float.fromJSON(month),
py.float.fromJSON(day)
]);
},
__radd__: function (other) {
return this.__add__(other);
},
__sub__: function (other) {
if (!(other instanceof datetime.date)) {
if (py.PY_call(py.isinstance, [datetime.date]) !== py.True) {
return py.NotImplemented;
}
// TODO: test this whole mess
@ -136,7 +226,11 @@ openerp.web.pyeval = function (instance) {
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
return new datetime.date(year, month, day);
return py.PY_call(datetime.date, [
py.float.fromJSON(year),
py.float.fromJSON(month),
py.float.fromJSON(day)
]);
},
__rsub__: function (other) {
return this.__sub__(other);
@ -219,11 +313,12 @@ openerp.web.pyeval = function (instance) {
instance.web.pyeval.context = function () {
return {
uid: new py.float(instance.session.uid),
uid: py.float.fromJSON(instance.session.uid),
datetime: datetime,
time: time,
relativedelta: relativedelta,
current_date: date.today.__call__().strftime(['%Y-%m-%d']),
current_date: py.PY_call(
time.strftime, [py.str.fromJSON('%Y-%m-%d')]),
};
};

View File

@ -1,9 +1,51 @@
$(document).ready(function () {
var openerp;
module("eval.types", {
setup: function () {
openerp = window.openerp.testing.instanceFor('coresetup');
openerp.session.uid = 42;
}
});
test('datetime.datetime', function () {
});
test('datetime.date', function () {
});
test('datetime.timedelta', function () {
var d = new Date();
d.setUTCDate(d.getUTCDate() - 15);
strictEqual(
py.eval("(datetime.date.today()" +
"- datetime.timedelta(days=15))" +
".strftime('%Y-%m-%d')", openerp.web.pyeval.context()),
_.str.strftime('%04d-%02d-%02d',
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate()));
});
test('relativedelta', function () {
});
test('strftime', function () {
var d = new Date();
var context = openerp.web.pyeval.context();
strictEqual(
py.eval("time.strftime('%Y')", context),
String(d.getUTCFullYear()));
strictEqual(
py.eval("time.strftime('%Y')+'-01-30'", context),
String(d.getUTCFullYear()) + '-01-30');
strictEqual(
py.eval("time.strftime('%Y-%m-%d %H:%M:%S')", context),
_.str.sprintf('%04d-%02d-%02d %02d:%02d:%02d',
d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(),
d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()));
});
module("eval.contexts", {
setup: function () {
openerp = window.openerp.testing.instanceFor('coresetup');
openerp.session.uid = 42;
}
});
test('context_sequences', function () {
@ -135,6 +177,7 @@ $(document).ready(function () {
setup: function () {
openerp = window.openerp.testing.instanceFor('coresetup');
window.openerp.web.dates(openerp);
openerp.session.uid = 42;
}
});
test('current_date', function () {
@ -150,6 +193,7 @@ $(document).ready(function () {
module('eval.groupbys', {
setup: function () {
openerp = window.openerp.testing.instanceFor('coresetup');
openerp.session.uid = 42;
}
});
test('groupbys_00', function () {