├── .gitignore
├── LICENSE
├── README.md
├── clamp
├── __init__.py
├── build.py
├── commands.py
├── declarative.py
├── proxymaker.py
└── signature.py
├── ez_setup.py
├── setup.py
└── tests
└── integ
├── README.md
├── clamp_samples
├── __init__.py
├── callable.py
└── const_.py
├── ez_setup.py
├── junit_tests
├── TestCallable.java
└── TestConstant.java
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | *.class
3 | *.jar
4 | *.tar.gz
5 | *.egg
6 | build/*
7 | clamp.egg-info/
8 | dist/*
9 | *~
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction, and
10 | distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright
13 | owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all other entities
16 | that control, are controlled by, or are under common control with that entity.
17 | For the purposes of this definition, "control" means (i) the power, direct or
18 | indirect, to cause the direction or management of such entity, whether by
19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
20 | outstanding shares, or (iii) beneficial ownership of such entity.
21 |
22 | "You" (or "Your") shall mean an individual or Legal Entity exercising
23 | permissions granted by this License.
24 |
25 | "Source" form shall mean the preferred form for making modifications, including
26 | but not limited to software source code, documentation source, and configuration
27 | files.
28 |
29 | "Object" form shall mean any form resulting from mechanical transformation or
30 | translation of a Source form, including but not limited to compiled object code,
31 | generated documentation, and conversions to other media types.
32 |
33 | "Work" shall mean the work of authorship, whether in Source or Object form, made
34 | available under the License, as indicated by a copyright notice that is included
35 | in or attached to the work (an example is provided in the Appendix below).
36 |
37 | "Derivative Works" shall mean any work, whether in Source or Object form, that
38 | is based on (or derived from) the Work and for which the editorial revisions,
39 | annotations, elaborations, or other modifications represent, as a whole, an
40 | original work of authorship. For the purposes of this License, Derivative Works
41 | shall not include works that remain separable from, or merely link (or bind by
42 | name) to the interfaces of, the Work and Derivative Works thereof.
43 |
44 | "Contribution" shall mean any work of authorship, including the original version
45 | of the Work and any modifications or additions to that Work or Derivative Works
46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work
47 | by the copyright owner or by an individual or Legal Entity authorized to submit
48 | on behalf of the copyright owner. For the purposes of this definition,
49 | "submitted" means any form of electronic, verbal, or written communication sent
50 | to the Licensor or its representatives, including but not limited to
51 | communication on electronic mailing lists, source code control systems, and
52 | issue tracking systems that are managed by, or on behalf of, the Licensor for
53 | the purpose of discussing and improving the Work, but excluding communication
54 | that is conspicuously marked or otherwise designated in writing by the copyright
55 | owner as "Not a Contribution."
56 |
57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf
58 | of whom a Contribution has been received by Licensor and subsequently
59 | incorporated within the Work.
60 |
61 | 2. Grant of Copyright License.
62 |
63 | Subject to the terms and conditions of this License, each Contributor hereby
64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
65 | irrevocable copyright license to reproduce, prepare Derivative Works of,
66 | publicly display, publicly perform, sublicense, and distribute the Work and such
67 | Derivative Works in Source or Object form.
68 |
69 | 3. Grant of Patent License.
70 |
71 | Subject to the terms and conditions of this License, each Contributor hereby
72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
73 | irrevocable (except as stated in this section) patent license to make, have
74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where
75 | such license applies only to those patent claims licensable by such Contributor
76 | that are necessarily infringed by their Contribution(s) alone or by combination
77 | of their Contribution(s) with the Work to which such Contribution(s) was
78 | submitted. If You institute patent litigation against any entity (including a
79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a
80 | Contribution incorporated within the Work constitutes direct or contributory
81 | patent infringement, then any patent licenses granted to You under this License
82 | for that Work shall terminate as of the date such litigation is filed.
83 |
84 | 4. Redistribution.
85 |
86 | You may reproduce and distribute copies of the Work or Derivative Works thereof
87 | in any medium, with or without modifications, and in Source or Object form,
88 | provided that You meet the following conditions:
89 |
90 | You must give any other recipients of the Work or Derivative Works a copy of
91 | this License; and
92 | You must cause any modified files to carry prominent notices stating that You
93 | changed the files; and
94 | You must retain, in the Source form of any Derivative Works that You distribute,
95 | all copyright, patent, trademark, and attribution notices from the Source form
96 | of the Work, excluding those notices that do not pertain to any part of the
97 | Derivative Works; and
98 | If the Work includes a "NOTICE" text file as part of its distribution, then any
99 | Derivative Works that You distribute must include a readable copy of the
100 | attribution notices contained within such NOTICE file, excluding those notices
101 | that do not pertain to any part of the Derivative Works, in at least one of the
102 | following places: within a NOTICE text file distributed as part of the
103 | Derivative Works; within the Source form or documentation, if provided along
104 | with the Derivative Works; or, within a display generated by the Derivative
105 | Works, if and wherever such third-party notices normally appear. The contents of
106 | the NOTICE file are for informational purposes only and do not modify the
107 | License. You may add Your own attribution notices within Derivative Works that
108 | You distribute, alongside or as an addendum to the NOTICE text from the Work,
109 | provided that such additional attribution notices cannot be construed as
110 | modifying the License.
111 | You may add Your own copyright statement to Your modifications and may provide
112 | additional or different license terms and conditions for use, reproduction, or
113 | distribution of Your modifications, or for any such Derivative Works as a whole,
114 | provided Your use, reproduction, and distribution of the Work otherwise complies
115 | with the conditions stated in this License.
116 |
117 | 5. Submission of Contributions.
118 |
119 | Unless You explicitly state otherwise, any Contribution intentionally submitted
120 | for inclusion in the Work by You to the Licensor shall be under the terms and
121 | conditions of this License, without any additional terms or conditions.
122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of
123 | any separate license agreement you may have executed with Licensor regarding
124 | such Contributions.
125 |
126 | 6. Trademarks.
127 |
128 | This License does not grant permission to use the trade names, trademarks,
129 | service marks, or product names of the Licensor, except as required for
130 | reasonable and customary use in describing the origin of the Work and
131 | reproducing the content of the NOTICE file.
132 |
133 | 7. Disclaimer of Warranty.
134 |
135 | Unless required by applicable law or agreed to in writing, Licensor provides the
136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
138 | including, without limitation, any warranties or conditions of TITLE,
139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
140 | solely responsible for determining the appropriateness of using or
141 | redistributing the Work and assume any risks associated with Your exercise of
142 | permissions under this License.
143 |
144 | 8. Limitation of Liability.
145 |
146 | In no event and under no legal theory, whether in tort (including negligence),
147 | contract, or otherwise, unless required by applicable law (such as deliberate
148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be
149 | liable to You for damages, including any direct, indirect, special, incidental,
150 | or consequential damages of any character arising as a result of this License or
151 | out of the use or inability to use the Work (including but not limited to
152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or
153 | any and all other commercial damages or losses), even if such Contributor has
154 | been advised of the possibility of such damages.
155 |
156 | 9. Accepting Warranty or Additional Liability.
157 |
158 | While redistributing the Work or Derivative Works thereof, You may choose to
159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or
160 | other liability obligations and/or rights consistent with this License. However,
161 | in accepting such obligations, You may act only on Your own behalf and on Your
162 | sole responsibility, not on behalf of any other Contributor, and only if You
163 | agree to indemnify, defend, and hold each Contributor harmless for any liability
164 | incurred by, or claims asserted against, such Contributor by reason of your
165 | accepting any such warranty or additional liability.
166 |
167 | END OF TERMS AND CONDITIONS
168 |
169 | APPENDIX: How to apply the Apache License to your work
170 |
171 | To apply the Apache License to your work, attach the following boilerplate
172 | notice, with the fields enclosed by brackets "[]" replaced with your own
173 | identifying information. (Don't include the brackets!) The text should be
174 | enclosed in the appropriate comment syntax for the file format. We also
175 | recommend that a file or class name and description of purpose be included on
176 | the same "printed page" as the copyright notice for easier identification within
177 | third-party archives.
178 |
179 | Copyright [yyyy] [name of copyright owner]
180 |
181 | Licensed under the Apache License, Version 2.0 (the "License");
182 | you may not use this file except in compliance with the License.
183 | You may obtain a copy of the License at
184 |
185 | http://www.apache.org/licenses/LICENSE-2.0
186 |
187 | Unless required by applicable law or agreed to in writing, software
188 | distributed under the License is distributed on an "AS IS" BASIS,
189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
190 | See the License for the specific language governing permissions and
191 | limitations under the License.
192 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Clamp background
2 | ================
3 |
4 | Clamp is part of the jythontools project
5 | (https://github.com/jythontools). Although Jython integrates well with
6 | Java, Clamp improves this support by enabling precise generation of
7 | the Java bytecode used to wrap Python classes. In a nutshell, this
8 | means such clamped classes can be used as modern Java classes.
9 |
10 | Clamp integrates with setuptools. Clamped packages are installed into
11 | site-packages. Clamp can also take an entire Jython installation,
12 | including site-packages, and wrap it into a **single jar**.
13 |
14 | Clamp thereby provides the following benefits:
15 |
16 | * JVM frameworks and containers can readily work with clamped code,
17 | oblivious of its source
18 |
19 | * This is especially true of those frameworks that need single jar
20 | support
21 |
22 | * Developers can stay as much in Python as possible. Clamp
23 | simply requires that any clamped Python classes inherit from a Java base class
24 | and/or extend Java interfaces. We may relax this restriction in the future.
25 |
26 | * Clamp uses a SQLAlchemy-like DSL that is declarative, using
27 | metaclasses and other metaprogramming techniques. We also plan to
28 | extend this DSL substantially in the future.
29 |
30 |
31 | Clamp example: Clamped
32 | ======================
33 |
34 | Please see the "Clamped" project on how to use this package. This
35 | project provides crucial documentation on how to use Clamp by going
36 | through an example in detail in the README:
37 | https://github.com/jimbaker/clamped
38 |
39 | The [Clamped README][clamped] also details some aspects of the
40 | bytecode generation and how it enables direct Java usage.
41 |
42 | Lastly, there is a [talk][] on Clamp available
43 | ([source][talk source]). Note that this talk goes more into the
44 | implementation of Clamp, including how we use metaprogramming.
45 |
46 |
47 | Important caveats
48 | =================
49 |
50 | Clamp has proven to be useful in production environments, but it needs
51 | additional work before we can announce a finalized, stable release
52 | (a 1.0 in other words). In particular, it should be possible to run
53 | Clamp on Windows environments. In addition, we need a robust test
54 | suite, in addition to the functional testing we are doing by
55 | hand. Once we have such a test suite, we plan to post on PyPI.
56 |
57 | Expect to see more updates once we complete the release of
58 | Jython 2.7.0, which has been keeping us from spending more time on
59 | this project.
60 |
61 |
62 | Usage
63 | =====
64 |
65 | Installation
66 | ------------
67 |
68 | Start by installing Jython 2.7. Clamp does not currently work on Windows, so the
69 | most recent beta 4 will work for you. Get it at the
70 | [Jython website][]. You will want to bootstrap pip (this next step
71 | will be part of the Jython installer by the final release):
72 |
73 | ````bash
74 | $ jython -m ensurepip
75 | ````
76 |
77 | With this step, the pip command is now available in
78 | `$JYTHON_HOME/bin/pip`. You may want to alias `$JYTHON_HOME/bin/pip`
79 | as `jpip`, or you can use [pyenv][] to use it alongside CPython's
80 | pip. Your choice. In the example below, we use `jpip` to keep it
81 | unambiguous which one you are using:
82 |
83 | ````bash
84 | jpip install git+https://github.com/jythontools/clamp.git
85 | ````
86 |
87 | Now Clamp is installed.
88 |
89 |
90 | Setuptools integration
91 | ----------------------
92 |
93 | The clamp project uses setuptools integration. You simply need to
94 | add one keyword, `clamp`, as well as depend on the Clamp package:
95 |
96 | ````python
97 | import ez_setup
98 | ez_setup.use_setuptools()
99 |
100 | from setuptools import setup, find_packages
101 |
102 |
103 | setup(
104 | name = "clamped",
105 | version = "0.1",
106 | packages = find_packages(),
107 | install_requires = ["clamp"],
108 | clamp = {
109 | "modules": ["clamped"]
110 | }
111 | )
112 | ````
113 |
114 | At a minimum, you need to specify with `modules` any modules you wish
115 | to clamp. This will result in clamp attempting to import each module,
116 | then saving any generated Java bytecode for clamped classes into a
117 | constructed jar.
118 |
119 |
120 | Example clamped class
121 | ---------------------
122 |
123 | Your class (currently) needs to implement Java interfaces and/or
124 | extend a Java class - this inheritance scheme ensures that Java code
125 | knows how to use your clamped class. Your clamped class also needs to
126 | be imported by one of the modules you specified with `clamp.modules`,
127 | so that Clamp can generate the necessary proxy bytecode.
128 |
129 | Your class also needs to use a base class generated by `clamp_base` to
130 | provide the mapping to a specific Java package namespace. You can
131 | choose an arbitrarily nested prefix, such as `com.example.bar.baz.foo`:
132 |
133 | ````python
134 | from java.io import Serializable
135 | from java.util.concurrent import Callable
136 |
137 | from clamp import clamp_base
138 |
139 | BarBase = clamp_base("bar")
140 |
141 |
142 | class BarClamp(BarBase, Callable, Serializable):
143 |
144 | def __init__(self):
145 | print "Being init-ed", self
146 |
147 | def call(self):
148 | print "foo"
149 | return 42
150 | ````
151 |
152 | From Java, your class is now available and directly importable as
153 | `bar.clamped.BarClamp` - the package prefix + the module name
154 | (possibly nested) + the class name. See the Clamped example project
155 | for more usage info.
156 |
157 |
158 | `clamp` command
159 | ---------------
160 |
161 | The `clamp` command performs the following operations:
162 |
163 | * Constructs a jar for all clamped classes specified by
164 | `clamp.modules`, per the above setup.py.
165 |
166 | * Copies into site-packages any jars embedded in the clamped
167 | package. So these are usually jars that your Python code depends
168 | upon - "third-party jars". Note at this time, Maven and other
169 | package managers are not yet supported - you have to explicitly
170 | embed any necessary jars.
171 |
172 | * Registers both types of jars (constructed, embdded) in jar.pth so
173 | that they are available for import. (By using a pth file, we ensure
174 | that they are referenceable on `sys.path`.)
175 |
176 | You should run the `clamp` command after running the `install` command:
177 |
178 | ````bash
179 | $ jython27 setup.py install
180 | $ jython27 setup.py clamp
181 | ````
182 |
183 | Currently this results in a layout in site-packages as
184 | follows. Ideally, the jars would be placed in the egg (unzipped), but
185 | setuptools does not like files it does not control in eggs. Regardless
186 | this layout is certainly subject to change to make it better:
187 |
188 | ````
189 | $ tree site-packages/
190 | site-packages/
191 | ├── README
192 | ├── clamp-0.4-py2.7.egg
193 | ├── clamped
194 | │ └── clamped
195 | │ └── javalib
196 | │ └── baz-4.2.jar
197 | ├── clamped-0.1-py2.7.egg
198 | │ ├── EGG-INFO
199 | │ │ ├── PKG-INFO
200 | │ │ ├── SOURCES.txt
201 | │ │ ├── dependency_links.txt
202 | │ │ ├── not-zip-safe
203 | │ │ ├── requires.txt
204 | │ │ └── top_level.txt
205 | │ └── clamped
206 | │ ├── __init__$py.class
207 | │ ├── __init__.py
208 | │ └── data
209 | │ └── example.txt
210 | ├── easy-install.pth
211 | ├── jar.pth
212 | ├── jars
213 | │ └── clamped-0.1.jar
214 | ├── setuptools-2.1-py2.7.egg
215 | └── setuptools.pth
216 | ````
217 |
218 | What is not possible - without extremely invasive (and brittle) code -
219 | is to monkeypatch the `install` command for users of the Clamp
220 | package. The [Paver project][] is one possible alternative.
221 |
222 |
223 | `build_jar` command
224 | -------------------
225 |
226 | This command is not normally needed (as of 0.4), since the `clamp`
227 | command subsumes this functionality.
228 |
229 | To create a jar for a clamped module in site-packages/jars and
230 | register this new jar in site-packages/jar.pth:
231 |
232 | ````bash
233 | $ jython27 setup.py build_jar
234 | ````
235 |
236 |
237 | `singlejar` command
238 | -------------------
239 |
240 | Use the `singlejar` command to create a single jar version of the
241 | current Jython installation. (This will include virtualenv
242 | environments, but note that virtualenv support for Jython 2.7 needs
243 | some additional work. If you are building Jython from source at this
244 | time, just use that directory for now.) This setup.py custom command
245 | will use the project name as the base for the jar:
246 |
247 | ````bash
248 | $ jython27 setup.py singlejar
249 | ````
250 |
251 | To create a single jar version of the current Jython installation, you
252 | can also run this script, which is installed in Jython's bin
253 | directory. By default, the jar is named `jython-single.jar`
254 |
255 | ````bash
256 | $ bin/singlejar # same, but outputs jython-single.jar,
257 | ````
258 |
259 | A number of options are supported:
260 |
261 | ````
262 | bin/singlejar --help
263 | usage: singlejar [-h] [--output PATH] [--classpath CLASSPATH] [--runpy PATH]
264 |
265 | create a singlejar of all Jython dependencies, including clamped jars
266 |
267 | optional arguments:
268 | -h, --help show this help message and exit
269 | --output PATH, -o PATH
270 | write jar to output path
271 | --classpath CLASSPATH
272 | jars to include in addition to Jython runtime and
273 | site-packages jars
274 | --runpy PATH, -r PATH
275 | path to __run__.py to make a runnable jar
276 | ````
277 |
278 |
279 | TODO
280 | ====
281 |
282 | * Add support for [variadic constructors](#variadic-constructors) of
283 | clamped classes. This means that in Java, using code can simply
284 | perform `new BarClamp(x, y, ...)`; in Python, `BarClamp(x, y, ...)`.
285 |
286 | * Provide basic support for annotations.
287 |
288 | * [Annotation magic](#supporting-java-annotations). It would be nice to import
289 | annotations into Python, use as class decorators and function
290 | decorators, and then still compile a Java class that works.
291 |
292 | * Instance fields support, comparable to `__slots__`, but baked into
293 | the emitted Java class. Such support would directly enable emitted
294 | clases to be used as POJOs by using Java code. Clamp should use
295 | `__slots__` if available. However, without further information, this
296 | would mean emitting fields of type `Object`. So there should be also
297 | some way of constraining the types of emitted instance fields in
298 | `ClampProxyMaker`. Likely this should be as simple as a new `slots`
299 | keyword when creating a proxymaker that simply maps fields to Java
300 | types.
301 |
302 | * Map [Python descriptors][] to Java's convention of getters/setters. Note
303 | that `__delete__` is not a mappable idea!
304 |
305 | * Add support for resolving external jars with Maven.
306 |
307 | * Standalone jar support in Jython itself does not currently support
308 | `.pth` files and consequently `site-packages`. Clamp works around
309 | this by packaging everything in `Lib/`, but this is not desirable
310 | due to possible collisions. This means the possibility of subtle
311 | changes in class loader resolution, compared to what Jython offers
312 | with `sys.path`.
313 |
314 | Moreover, it would be nice if jars in `site-packages`
315 | could simply be included directly without unpacking.
316 |
317 | * The `singlejar` command should generate Jython cache info on all
318 | included files and bundle in the generated uber jar. It's not clear
319 | how readily this precaching can be done on a per-jar basis with
320 | `build_jar`, but cache data is per jar; see
321 | `{python.cachedir}/packages/*.pkc`; the corresponding code in
322 | Jython's internals is in `org.python.core.packagecache`.
323 |
324 | * Testing and placement in PyPI. Due to the bytecode construction,
325 | writing unit tests for this type of functionality seems to be
326 | nontrivial, but still very much needed to move this from an initial
327 | spike to not being in a pre-alpha stage.
328 |
329 |
330 | Known issues
331 | ============
332 |
333 | It's not feasible to use `__new__` in your Python classes that are
334 | clamped. Why not? Java expects that constructing an object for a given
335 | class returns an object of that class! The solution is simple: call a
336 | factory function, in Python or Java, to return arbitrary objects. This
337 | is just a simple, but fundamental, mismatch between Python and Java in
338 | its object model.
339 |
340 | A related issue is that you cannot change the `__class__` of an
341 | instance of clamped class.
342 |
343 |
344 | Variadic constructors
345 | =====================
346 |
347 | Clamp currently supports no-arg constructors of clamped classes, as
348 | seen in the generated code below for a Jython proxy:
349 |
350 | ````java
351 | public BarClamp() {
352 | super();
353 | this.__initProxy__(Py.EmptyObjects);
354 | }
355 | ````
356 |
357 | Note that it should be a simple matter to add variadic constructors,
358 | eg `BarClamp(Object... args)`, by using the underlying support in
359 | `__initProxy__`, also generated in Jython proxies:
360 |
361 | ````java
362 | public void __initProxy__(final Object[] array) {
363 | Py.initProxy((PyProxy)this, "clamped", "BarClamp", array);
364 | }
365 | ````
366 |
367 | This should be as simple as using `ClassFile.addMethod` to generate
368 | the following code:
369 |
370 | ````java
371 | public BarClamp(Object[] args) {
372 | super(args);
373 | this.__initProxy__(args);
374 | }
375 | ````
376 |
377 | `__initProxy__` will in turn take care of boxing any args as
378 | `PyObject` args.
379 |
380 |
381 | Supporting Java annotations
382 | ===========================
383 |
384 | Java annotations are widely used in contemporary Java code. Following
385 | an example in the Apache Quartz documentation, in Quartz one might
386 | write the following in Java:
387 |
388 | ````java
389 | @PersistJobDataAfterExecution
390 | @DisallowConcurrentExecution
391 | public class ColorJob implements Job {
392 | ...
393 | }
394 | ````
395 |
396 | Compiled usage of such annotations is very simple: they simply are
397 | part of the metadata of the class. As metadata, they are then used for
398 | metaprogramming at the Java level, eg, to support introspection or
399 | bytecode rewriting.
400 |
401 | It would seem that class decorators would be the natural analogue to
402 | writing this in Jython:
403 |
404 | ````python
405 | @PersistJobDataAfterExecution
406 | @DisallowConcurrentExecution
407 | class ColorJob(Job):
408 | ...
409 | ````
410 |
411 | But there are a few problems. First, Java annotations are
412 | interfaces. To solve, clamp can support a module, let's call it
413 | `clamp.magic`, which when imported, will intercept any subsequent
414 | imports of Java class/method annotations and turn them into class
415 | decorators/function decorators. This requires the top-level script of
416 | `clamp.magic` to insert an appropriate meta importer to
417 | `sys.meta_path`, as described in [PEP 302][].
418 |
419 | Next, class decorators are applied *after* type construction in
420 | Python. The solution is for such class decorators to transform
421 | (rewrite) the bytecode for generated Java class to add any desired
422 | annotations, then save it under the original class name. Such
423 | transformations can be readily done with the ASM package by using an
424 | [AnnotationVisitor][]
, as documented in section 4.2 of
425 | the [ASM user guide][].
426 |
427 | Lastly, saving under the original class name requires a little more
428 | work, because currently all generated classes in Clamp are directly
429 | written using `JarOutputStream`; simply resaving will result in a
430 | `ZipException` of `"duplicate entry"`. This simply requires deferring
431 | the write of a module, including any supporting Java classes, until
432 | the top-level script of the module has completed.
433 |
434 | Mapping method annotations to function decorators should likewise be
435 | straightforward. Field annotations currently would only correspond to
436 | static fields, which has direct support in Clamp - there's no Python
437 | syntax equivalent.
438 |
439 |
440 |
441 |
442 | [AnnotationVisitor]: http://asm.ow2.org/asm40/javadoc/user/org/objectweb/asm/AnnotationVisitor.html
443 | [ASM user guide]: http://download.forge.objectweb.org/asm/asm4-guide.pdf
444 | [clamped]: https://github.com/jimbaker/clamped
445 | [Jython website]: http://www.jython.org/
446 | [Paver project]: http://pythonhosted.org/Paver/
447 | [PEP 302]: http://www.python.org/dev/peps/pep-0302/
448 | [pyenv]: https://github.com/yyuu/pyenv
449 | [Python descriptors]: http://docs.python.org/2/howto/descriptor.html
450 | [talk]: https://github.com/jimbaker/clamped/blob/master/talk.pdf
451 | [talk source]: https://github.com/jimbaker/clamped/blob/master/talk.md
452 |
--------------------------------------------------------------------------------
/clamp/__init__.py:
--------------------------------------------------------------------------------
1 | from clamp.declarative import clamp_base, Constant
2 | from clamp.proxymaker import ClampProxyMaker
3 |
4 |
5 | __all__ = ["clamp_base", "ClampProxyMaker", "Constant"]
6 |
--------------------------------------------------------------------------------
/clamp/build.py:
--------------------------------------------------------------------------------
1 | # NOTE the obivious lack of support for simultaneous installs. If in
2 | # fact we need to do this, implement an obvious advisory locking
3 | # scheme when writing files like jar.pth
4 |
5 | import distutils
6 | import glob
7 | import jarray
8 | import os
9 | import os.path
10 | import site
11 | import sys
12 | import time
13 | import logging
14 |
15 | from collections import OrderedDict
16 | from contextlib import closing, contextmanager # FIXME need to merge in Java 7 support for AutoCloseable
17 | from java.io import BufferedInputStream, FileInputStream
18 | from java.util.jar import Attributes, JarEntry, JarInputStream, JarOutputStream, Manifest
19 | from java.util.zip import ZipException, ZipInputStream
20 |
21 | log = logging.getLogger(__name__)
22 |
23 |
24 | class NullBuilder(object):
25 |
26 | def __repr__(self):
27 | return "NullBuilder"
28 |
29 | def write_class_bytes(self, package, classname, bytes):
30 | pass
31 |
32 |
33 | _builder = NullBuilder = NullBuilder()
34 |
35 |
36 | @contextmanager
37 | def register_builder(builder):
38 | global _builder
39 | log.debug("Registering builder %r, old builder was %r", builder, _builder)
40 | old_builder = _builder
41 | _builder = builder
42 | yield
43 | _builder = old_builder
44 |
45 |
46 | def get_builder():
47 | return _builder
48 |
49 |
50 | # probably refactor in a class
51 |
52 | def get_package_name(path):
53 | return "-".join(os.path.split(path)[1].split("-")[:-1])
54 |
55 |
56 | def read_pth(pth_path):
57 | paths = OrderedDict()
58 | if os.path.exists(pth_path):
59 | with open(pth_path) as pth:
60 | for path in pth:
61 | path = path.strip()
62 | if path.startswith("#") or path.startswith("import "):
63 | continue # FIXME consider preserving comments, other user changes
64 | name = get_package_name(os.path.split(path)[1])
65 | paths[name] = path
66 | return paths
67 |
68 |
69 | class JarPth(object):
70 | def __init__(self):
71 | self._jar_pth_path = os.path.join(site.getsitepackages()[0], "jar.pth")
72 | self._paths = read_pth(self._jar_pth_path)
73 | self._mutated = False
74 | log.debug("paths in jar.pth %s are %r", self._jar_pth_path, self)
75 |
76 | def __enter__(self):
77 | return self
78 |
79 | def __exit__ (self, type, value, tb):
80 | self.close()
81 |
82 | def _write_jar_pth(self):
83 | if self._mutated:
84 | with open(self._jar_pth_path, "w") as jar_pth:
85 | for name, path in sorted(self.iteritems()):
86 | jar_pth.write(path + "\n")
87 |
88 | def close(self):
89 | self._write_jar_pth()
90 |
91 | def __getitem__(self, key):
92 | return self._paths[key]
93 |
94 | def __setitem__(self, key, value):
95 | self._paths[key] = value
96 | self._mutated = True
97 |
98 | def __delitem__(self, key):
99 | del self._paths[key]
100 | self._mutated = True
101 |
102 | def __contains__(self, key):
103 | return key in self._paths
104 |
105 | def __len__(self):
106 | return len(self._paths)
107 |
108 | def __repr__(self):
109 | return repr(self._paths)
110 |
111 | def __iter__(self):
112 | return self._paths.__iter__()
113 |
114 | def iterkeys(self):
115 | return self._paths.iterkeys()
116 |
117 | def itervalues(self):
118 | return self._paths.itervalues()
119 |
120 | def iteritems(self):
121 | return self._paths.iteritems()
122 |
123 |
124 | # Default location for storing clamped jars
125 | def init_jar_dir():
126 | jar_dir = os.path.join(site.getsitepackages()[0], "jars")
127 | if not os.path.exists(jar_dir):
128 | os.mkdir(jar_dir)
129 | return jar_dir
130 |
131 |
132 | class OutputJar(object):
133 |
134 | # Derived, with heavy modifications, from
135 | # http://stackoverflow.com/questions/1281229/how-to-use-jaroutputstream-to-create-a-jar-file
136 |
137 | def __init__(self, jar=None, output_path="output.jar"):
138 | self.output_path = output_path
139 | if jar is not None:
140 | self.jar = jar
141 | self.output = None
142 | return
143 | self.runpy = None
144 | self.setup()
145 |
146 | def __enter__ (self):
147 | return self
148 |
149 | def __exit__ (self, type, value, tb):
150 | self.close()
151 |
152 | def setup(self):
153 | manifest = Manifest()
154 | manifest.getMainAttributes()[Attributes.Name.MANIFEST_VERSION] = "1.0"
155 | if self.runpy and os.path.exists(self.runpy):
156 | manifest.getMainAttributes()[Attributes.Name.MAIN_CLASS] = "org.python.util.JarRunner"
157 | else:
158 | log.debug("No __run__.py defined, so defaulting to Jython command line")
159 | manifest.getMainAttributes()[Attributes.Name.MAIN_CLASS] = "org.python.util.jython"
160 |
161 | self.output = open(self.output_path, "wb")
162 | self.jar = JarOutputStream(self.output, manifest)
163 | self.created_paths = set()
164 | self.build_time = int(time.time() * 1000)
165 |
166 | def close(self):
167 | self.jar.close()
168 | if self.output:
169 | self.output.close()
170 |
171 | def create_ancestry(self, path_parts):
172 | for i in xrange(len(path_parts), 0, -1): # right to left
173 | ancestor = "/".join(path_parts[:-i]) + "/"
174 | if ancestor == "/":
175 | continue # FIXME shouldn't need to do this special casing
176 | if ancestor not in self.created_paths:
177 | entry = JarEntry(ancestor)
178 | entry.time = self.build_time
179 | try:
180 | self.jar.putNextEntry(entry)
181 | self.jar.closeEntry()
182 | except ZipException, e:
183 | if not "duplicate entry" in str(e):
184 | log.error("Problem in creating entry %r", entry, exc_info=True)
185 | raise
186 | self.created_paths.add(ancestor)
187 |
188 |
189 | class JarCopy(OutputJar):
190 |
191 | def __init__(self, jar=None, output_path="output.jar", runpy=None):
192 | self.output_path = output_path
193 | if jar is not None:
194 | self.jar = jar
195 | self.output = None
196 | return
197 | self.runpy = runpy
198 | self.setup()
199 |
200 | def copy_zip_input_stream(self, zip_input_stream, parent=None):
201 | """Given a `zip_input_stream`, copy all entries to the output jar"""
202 |
203 | chunk = jarray.zeros(8192, "b")
204 | while True:
205 | entry = zip_input_stream.getNextEntry()
206 | if entry is None:
207 | break
208 | try:
209 | # NB: cannot simply use old entry because we need
210 | # to recompute compressed size
211 | if parent:
212 | name = "/".join([parent, entry.name])
213 | else:
214 | name = entry.name
215 | if name.startswith("META-INF/") and name.endswith(".SF"):
216 | # Skip signature files - by their nature, they do
217 | # not work when their source jars are copied
218 | log.debug("Skipping META-INF signature file %s", name)
219 | continue
220 | output_entry = JarEntry(name)
221 | output_entry.time = entry.time
222 | self.jar.putNextEntry(output_entry)
223 | while True:
224 | read = zip_input_stream.read(chunk, 0, 8192)
225 | if read == -1:
226 | break
227 | self.jar.write(chunk, 0, read)
228 | self.jar.closeEntry()
229 | except ZipException, e:
230 | if not "duplicate entry" in str(e):
231 | log.error("Problem in copying entry %r", output_entry, exc_info=True)
232 | raise
233 |
234 | def copy_jars(self, jars):
235 | """Consumes a sequence of jar paths, fixing up paths as necessary"""
236 | seen = set()
237 | for jar_path in jars:
238 | normed_path = os.path.realpath(os.path.normpath(jar_path))
239 | if os.path.splitext(normed_path)[1] != ".jar":
240 | log.warn("Will only copy jars, not %s", normed_path)
241 | next
242 | if normed_path in seen:
243 | next
244 | seen.add(normed_path)
245 | log.debug("Copying %s", normed_path)
246 | with open(normed_path) as f:
247 | with closing(JarInputStream(f)) as input_jar:
248 | self.copy_zip_input_stream(input_jar)
249 |
250 | def copy_file(self, relpath, path):
251 | path_parts = tuple(os.path.split(relpath)[0].split(os.sep))
252 | self.create_ancestry(path_parts)
253 | chunk = jarray.zeros(8192, "b")
254 | with open(path) as f:
255 | with closing(BufferedInputStream(f)) as bis:
256 | output_entry = JarEntry(relpath)
257 | output_entry.time = int(os.path.getmtime(path) * 1000)
258 | try:
259 | self.jar.putNextEntry(output_entry)
260 | while True:
261 | read = bis.read(chunk, 0, 8192)
262 | if read == -1:
263 | break
264 | self.jar.write(chunk, 0, read)
265 | self.jar.closeEntry()
266 | except ZipException, e:
267 | if not "duplicate entry" in str(e):
268 | log.error("Problem in creating entry %r", entry, exc_info=True)
269 | raise
270 |
271 |
272 | class JarBuilder(OutputJar):
273 |
274 | def __repr__(self):
275 | return "JarBuilder(output={!r})".format(self.output_path)
276 |
277 | def _canonical_path_parts(self, package, classname):
278 | return tuple(classname.split("."))
279 |
280 | def write_class_bytes(self, package, classname, bytes):
281 | path_parts = self._canonical_path_parts(package, classname)
282 | self.create_ancestry(path_parts)
283 | entry = JarEntry("/".join(path_parts) + ".class")
284 | entry.time = self.build_time
285 | self.jar.putNextEntry(entry)
286 | self.jar.write(bytes.toByteArray())
287 | self.jar.closeEntry()
288 |
289 |
290 | def find_jython_jars():
291 | """Uses the same classpath resolution as bin/jython"""
292 | jython_jar_path = os.path.normpath(os.path.join(sys.executable, "../../jython.jar"))
293 | jython_jar_dev_path = os.path.normpath(os.path.join(sys.executable, "../../jython-dev.jar"))
294 | if os.path.exists(jython_jar_dev_path):
295 | jars = [jython_jar_dev_path]
296 | jars.extend(glob.glob(os.path.normpath(os.path.join(jython_jar_dev_path, "../javalib/*.jar"))))
297 | elif os.path.exists(jython_jar_path):
298 | jars = [jython_jar_path]
299 | else:
300 | try:
301 | from org.python.util import jython
302 | jars = [jython().getClass().getProtectionDomain().getCodeSource().getLocation().getPath()]
303 | except:
304 | raise Exception("Cannot find jython jar")
305 | return jars
306 |
307 |
308 | def find_jython_lib_files():
309 | seen = set()
310 | sitepackages = site.getsitepackages()
311 | root = os.path.normpath(os.path.join(sys.executable, "../../Lib"))
312 | for dirpath, dirnames, filenames in os.walk(root, followlinks=True):
313 | ignore = False
314 | for pkg in sitepackages:
315 | if dirpath.startswith(pkg):
316 | ignore = True
317 | if ignore:
318 | continue
319 | for filename in filenames:
320 | path = os.path.join(dirpath, filename)
321 | relpath = path[len(root)-3:] # this will of course not work for included directories FIXME bad hack!
322 | yield relpath, os.path.realpath(path)
323 |
324 | # FIXME verify realpath, realpath(dirpath) has not been seen (no cycles!)
325 |
326 |
327 | def find_package_libs(root):
328 | for dirpath, dirnames, filenames in os.walk(root):
329 | for filename in filenames:
330 | path = os.path.join(dirpath, filename)
331 | relpath = path[len(root)+1:]
332 | yield relpath, path
333 |
334 |
335 | def copy_zip_file(path, output_jar):
336 | try:
337 | with open(path) as f:
338 | with closing(BufferedInputStream(f)) as bis:
339 | if not skip_zip_header(bis):
340 | return False
341 | with closing(ZipInputStream(bis)) as input_zip:
342 | try:
343 | output_jar.copy_zip_input_stream(input_zip, "Lib")
344 | return True
345 | except ZipException:
346 | return False
347 | except IOError:
348 | return False
349 |
350 |
351 | def skip_zip_header(bis):
352 | try:
353 | for i in xrange(2000):
354 | # peek ahead 2 bytes to look for PK in header
355 | bis.mark(2)
356 | first = chr(bis.read())
357 | second = chr(bis.read())
358 | if first == "P" and second == "K":
359 | bis.reset()
360 | return True
361 | else:
362 | bis.reset()
363 | bis.read()
364 | else:
365 | return False
366 | except ValueError: # consume -1 on unsuccessful read
367 | return False
368 |
369 |
370 | def build_jar(package_name, jar_name, clamp_setup, output_path=None):
371 | update_jar_pth = not(output_path)
372 | if output_path is None:
373 | jar_dir = init_jar_dir()
374 | output_path = os.path.join(jar_dir, jar_name)
375 | # Remove the old jar (if present) to prevent from being imported.
376 | #
377 | # Note that it will still be scanned by SysPackageManager,
378 | # because that happens before any user-level Python code (like
379 | # this module) can be run. This will be addressed when the
380 | # build preemptively creates such package cache data.
381 | try:
382 | sys.path.remove(output_path)
383 | except ValueError:
384 | pass
385 | try:
386 | os.remove(output_path)
387 | except OSError:
388 | pass
389 |
390 | with JarBuilder(output_path=output_path) as builder:
391 | with register_builder(builder):
392 | for module in clamp_setup.modules:
393 | __import__(module)
394 |
395 | if update_jar_pth:
396 | with JarPth() as paths:
397 | paths[package_name] = os.path.join("./jars", jar_name)
398 |
399 |
400 | def get_included_jars(src_dir, packages):
401 | prefix_length = len(src_dir)+1
402 | for package in packages:
403 | for dirpath, dirs, files in os.walk(os.path.join(src_dir, package)):
404 | for name in files:
405 | _, ext = os.path.splitext(name)
406 | if ext == ".jar":
407 | path = os.path.join(dirpath, name)
408 | yield path[prefix_length:]
409 |
410 |
411 | def copy_included_jars(package_name, packages, src_dir=None, dest_dir=None):
412 | # FIXME ideally dest_dir would be the corresponding egg, but this requires additional work
413 | # so that setuptools will not remove upon subsequent runs of setuptool commands
414 | if src_dir is None:
415 | src_dir = os.getcwd()
416 | if dest_dir is None:
417 | dest_dir = os.path.join(site.getsitepackages()[0], package_name)
418 | jar_files = sorted(get_included_jars(src_dir, packages))
419 | distutils.dir_util.create_tree(dest_dir, jar_files)
420 | for jar_file in jar_files:
421 | distutils.file_util.copy_file(os.path.join(src_dir, jar_file), os.path.join(dest_dir, jar_file))
422 | with JarPth() as paths:
423 | for jar_file in jar_files:
424 | paths[jar_file] = os.path.join(".", package_name, jar_file)
425 | return [os.path.join(dest_dir, jar_file) for jar_file in jar_files]
426 |
427 |
428 | def create_singlejar(output_path, classpath, runpy):
429 | jars = classpath
430 | jars.extend(find_jython_jars())
431 | site_path = site.getsitepackages()[0]
432 | with JarPth() as jar_pth:
433 | for jar_path in sorted(jar_pth.itervalues()):
434 | jars.append(os.path.join(site_path, jar_path))
435 |
436 | with JarCopy(output_path=output_path, runpy=runpy) as singlejar:
437 | singlejar.copy_jars(jars)
438 | log.debug("Copying standard library")
439 | for relpath, realpath in find_jython_lib_files():
440 | singlejar.copy_file(relpath, realpath)
441 |
442 | # FOR NOW: copy everything in site-packages into Lib/ in the built jar;
443 | # this is because Jython in standalone mode has the limitation that it can
444 | # only properly find packages under Lib/ and cannot process .pth files
445 | # THIS SHOULD BE FIXED
446 |
447 | sitepackage = site.getsitepackages()[0]
448 |
449 | # copy top level packages
450 | for item in os.listdir(sitepackage):
451 | path = os.path.join(sitepackage, item)
452 | if path.endswith(".egg") or path.endswith(".egg-info") or path.endswith(".pth") or path == "jars":
453 | continue
454 | log.debug("Copying package %s", path)
455 | for pkg_relpath, pkg_realpath in find_package_libs(path):
456 | log.debug("Copy package file %s %s", pkg_relpath, pkg_realpath)
457 | singlejar.copy_file(os.path.join("Lib", item, pkg_relpath), pkg_realpath)
458 |
459 | # copy eggs
460 | for path in read_pth(os.path.join(sitepackage, "easy-install.pth")).itervalues():
461 | relpath = "/".join(os.path.normpath(os.path.join("Lib", path)).split(os.sep)) # ZIP only uses /
462 | path = os.path.realpath(os.path.normpath(os.path.join(sitepackage, path)))
463 |
464 | if copy_zip_file(path, singlejar):
465 | log.debug("Copying %s (zipped file)", path) # tiny lie - already copied, but keeping consistent!
466 | continue
467 |
468 | log.debug("Copying egg %s", path)
469 | for pkg_relpath, pkg_realpath in find_package_libs(path):
470 | # Filter out egg metadata
471 | parts = pkg_relpath.split(os.sep)
472 | head = parts[0]
473 | if head == "EGG-INFO" or head.endswith(".egg-info"):
474 | continue
475 | singlejar.copy_file(os.path.join("Lib", pkg_relpath), pkg_realpath)
476 |
477 | if runpy and os.path.exists(runpy):
478 | singlejar.copy_file("__run__.py", runpy)
479 |
--------------------------------------------------------------------------------
/clamp/commands.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import logging
3 | import os
4 | import os.path
5 | import setuptools
6 | import sys
7 | from contextlib import contextmanager
8 | from distutils.errors import DistutilsOptionError, DistutilsSetupError
9 | from setuptools.command.install import install
10 |
11 | from clamp.build import create_singlejar, build_jar, copy_included_jars
12 |
13 | logging.basicConfig()
14 | log = logging.getLogger("clamp")
15 |
16 |
17 | @contextmanager
18 | def honor_verbosity(verbose):
19 | if verbose > 1:
20 | old_level = log.getEffectiveLevel()
21 | log.setLevel(logging.DEBUG)
22 | yield
23 | if verbose > 1:
24 | log.setLevel(old_level)
25 |
26 |
27 | class ClampSetup(object):
28 |
29 | # FIXME include such things as excluded/included jars, etc
30 |
31 | def __init__(self, modules):
32 | self.modules = modules
33 |
34 |
35 | def parse_clamp_keyword(distribution, keyword, values):
36 | if keyword != "clamp":
37 | raise DistutilsSetupError("invalid keyword: {}".format(keyword))
38 | if "modules" not in values:
39 | raise DistutilsSetupError(
40 | "clamp={!r} is invalid: no 'modules' key present".format(values))
41 | distribution.zip_safe = False # given the use of jars
42 | try:
43 | invalid = []
44 | clamped_modules = list(values["modules"])
45 | for v in clamped_modules:
46 | if not isinstance(v, basestring):
47 | invalid.append(v)
48 | if invalid:
49 | raise DistutilsSetupError(
50 | "clamp={!r} is invalid: 'modules' key must be an iterable of importable module names".format(
51 | values))
52 | except TypeError, ex:
53 | log.error("Invalid clamp", exc_info=True)
54 | raise DistutilsSetupError("clamp={!r} is invalid: {}".format(values, ex))
55 | distribution.clamp = ClampSetup(clamped_modules)
56 |
57 |
58 | class build_jar_command(setuptools.Command):
59 |
60 | description = "create a jar for all clamped Python classes for this package"
61 | user_options = [
62 | ("output=", "o", "write jar to output path"),
63 | ]
64 |
65 | def initialize_options(self):
66 | self.output = None
67 |
68 | def finalize_options(self):
69 | if self.output is not None:
70 | dir_path = os.path.split(self.output)[0]
71 | if dir_path and not os.path.exists(dir_path):
72 | raise DistutilsOptionError("Directory {} to write jar must exist".format(dir_path))
73 | if os.path.splitext(self.output)[1] != ".jar":
74 | raise DistutilsOptionError("Path must be to a valid jar name, not {}".format(self.output))
75 |
76 | def get_jar_name(self):
77 | metadata = self.distribution.metadata
78 | return "{}-{}.jar".format(metadata.get_name(), metadata.get_version())
79 |
80 | def run(self):
81 | with honor_verbosity(self.distribution.verbose):
82 | if not self.distribution.clamp:
83 | raise DistutilsOptionError("Specify the modules to be built into a jar with the 'clamp' setup keyword")
84 | build_jar(self.distribution.metadata.get_name(),
85 | self.get_jar_name(), self.distribution.clamp, self.output)
86 |
87 |
88 | class clamp_command(install):
89 |
90 | description = "install required jars, run usual install, and clamp modules into jar"
91 |
92 | def get_jar_name(self):
93 | metadata = self.distribution.metadata
94 | return "{}-{}.jar".format(metadata.get_name(), metadata.get_version())
95 |
96 | def run(self):
97 | with honor_verbosity(self.distribution.verbose):
98 | if not self.distribution.clamp:
99 | raise DistutilsOptionError("Specify the modules to be built into a jar with the 'clamp' setup keyword")
100 |
101 | # 1. Ensure any included jars are immediately available
102 | available_paths = set(sys.path)
103 | jar_paths = copy_included_jars(self.distribution.metadata.get_name(), self.distribution.packages)
104 | for path in jar_paths:
105 | if path not in available_paths:
106 | print "Adding jar to sys.path", path
107 | sys.path.append(path) # make these jars are available
108 |
109 | # 2. Compile Python classes, which may depend on included jars.
110 | #
111 | # Use the underlying do_egg_install, which is invoked by
112 | # install.run in setuptools if it detects it is not in
113 | # legacy mode (using "slightly kludgy, but seems to work"
114 | # (!) frame inspection logic). We want to install an egg,
115 | # which supports pth, not legacy-supporting .egg-info.
116 | self.do_egg_install()
117 |
118 | # 3. Building clamped jar relies on both included jars and Python classes
119 | build_jar(self.distribution.metadata.get_name(),
120 | self.get_jar_name(), self.distribution.clamp)
121 |
122 |
123 | class singlejar_command(setuptools.Command):
124 |
125 | description = "create a singlejar of all Jython dependencies, including clamped jars"
126 | user_options = [
127 | ("output=", "o", "write jar to output path"),
128 | ("classpath=", None, "jars to include in addition to Jython runtime and site-packages jars"), # FIXME take a list?
129 | ("runpy=", "r", "path to __run__.py to make a runnable jar"),
130 | ]
131 |
132 | def initialize_options(self):
133 | metadata = self.distribution.metadata
134 | self.output = os.path.join(os.getcwd(), "{}-{}-single.jar".format(metadata.get_name(), metadata.get_version()))
135 | self.classpath = []
136 | self.runpy = os.path.join(os.getcwd(), "__run__.py")
137 |
138 | def finalize_options(self):
139 | # could validate self.output is a valid path FIXME
140 | if self.classpath:
141 | self.classpath = self.classpath.split(":")
142 |
143 | def run(self):
144 | with honor_verbosity(self.distribution.verbose):
145 | create_singlejar(self.output, self.classpath, self.runpy)
146 |
147 |
148 | def singlejar_script_command():
149 | parser = argparse.ArgumentParser(description="create a singlejar of all Jython dependencies, including clamped jars")
150 | parser.add_argument("--output", "-o", default="jython-single.jar", metavar="PATH",
151 | help="write jar to output path")
152 | parser.add_argument("--classpath", default=None,
153 | help="jars to include in addition to Jython runtime and site-packages jars")
154 | parser.add_argument("--runpy", "-r", default=os.path.join(os.getcwd(), "__run__.py"), metavar="PATH",
155 | help="path to __run__.py to make a runnable jar")
156 | args = parser.parse_args()
157 | if args.classpath:
158 | args.classpath = args.classpath.split(":")
159 | else:
160 | args.classpath = []
161 | create_singlejar(args.output, args.classpath, args.runpy)
162 |
--------------------------------------------------------------------------------
/clamp/declarative.py:
--------------------------------------------------------------------------------
1 | # Declarative should do all type inference, etc, in
2 | # ClampProxyMakerMeta (other than of course class decorator)
3 |
4 | from clamp.proxymaker import ClampProxyMaker
5 | from clamp.signature import Constant
6 |
7 |
8 | def clamp_base(package, proxy_maker=ClampProxyMaker):
9 | """ A helper method that allows you to create clamped classes
10 |
11 | Example::
12 |
13 | BarClamp = clamp_base(package='bar')
14 |
15 |
16 | class Test(BarClamp, Callable, Serializable):
17 |
18 | def __init__(self):
19 | print "Being init-ed", self
20 |
21 | def call(self):
22 | print "foo"
23 | return 42
24 | """
25 |
26 | def _clamp_closure(package, proxy_maker):
27 | """This closure sets the metaclass with our desired attributes
28 | """
29 | class ClampProxyMakerMeta(type):
30 |
31 | def __new__(cls, name, bases, dct):
32 | newdct = dict(dct)
33 | newdct['__proxymaker__'] = proxy_maker(package=package)
34 | return type.__new__(cls, name, bases, newdct)
35 |
36 | return ClampProxyMakerMeta
37 |
38 |
39 | class ClampBase(object):
40 | """Allows us not to have to set the __metaclass__ at all"""
41 | __metaclass__ = _clamp_closure(package=package, proxy_maker=proxy_maker)
42 |
43 | return ClampBase
44 |
--------------------------------------------------------------------------------
/clamp/proxymaker.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | import java
4 | from java.io import Serializable
5 | from java.lang.reflect import Modifier
6 | from org.python.core import Py
7 | from org.python.compiler import CustomMaker, ProxyCodeHelpers
8 | from org.python.util import CodegenUtils
9 |
10 | from clamp.build import get_builder
11 | from clamp.signature import Constant
12 |
13 |
14 | log = logging.getLogger(__name__)
15 |
16 |
17 | class SerializableProxyMaker(CustomMaker):
18 |
19 | # FIXME and push in docs presumably - in general, unless user otherwise specifies,
20 | # serialVersionUID of 1 is OK for python, thanks to dynamic
21 | # typing. Other errors -not having the right interface support
22 | # - will be caught earlier anyway.
23 |
24 | # NOTE: SerializableProxyMaker is itself a java proxy, but it's not a custom one!
25 |
26 | # TODO support fields in conjunction with property support in Python
27 |
28 | # (None,
29 | # array(java.lang.Class, [, ]),
30 | # u'BarClamp',
31 | # u'__main__',
32 | # u'clamped.__main__.BarClamp',
33 | # {'__init__': , '__module__': '__main__', 'call': , '__proxymaker__': }, 'clamped', {})
34 |
35 | def __init__(self, superclass, interfaces, className, pythonModuleName, fullProxyName, mapping, package, kwargs):
36 | self.package = package
37 | self.kwargs = kwargs
38 |
39 | log.debug("superclass=%s, interfaces=%s, className=%s, pythonModuleName=%s, fullProxyName=%s, mapping=%s, "
40 | "package=%s, kwargs=%s", superclass, interfaces, className, pythonModuleName, fullProxyName, mapping,
41 | package, kwargs)
42 |
43 | # FIXME break this out
44 | is_serializable = False
45 | inheritance = list(interfaces)
46 | if superclass:
47 | inheritance.append(superclass)
48 | for cls in inheritance:
49 | if issubclass(cls, Serializable):
50 | is_serializable = True
51 |
52 | if is_serializable:
53 | self.constants = { "serialVersionUID" : (java.lang.Long(1), java.lang.Long.TYPE) }
54 | else:
55 | self.constants = {}
56 | if "constants" in kwargs:
57 | self.constants.update(self.kwargs["constants"])
58 | self.updateConstantsFromMapping(mapping)
59 |
60 | CustomMaker.__init__(self, superclass, interfaces, className, pythonModuleName, fullProxyName, mapping)
61 |
62 | def updateConstantsFromMapping(self, mapping):
63 | """Looks for Constant in Object's dict and updates the constants, with appropriate values
64 | """
65 | for key, val in mapping.iteritems():
66 | if isinstance(val, Constant):
67 | if key in self.constants:
68 | log.warn("Constant with name %s is already declared, overriding", key)
69 | self.constants[key] = (val.value, val.type)
70 |
71 | def doConstants(self):
72 | # FIXME eg, self.constants = { "fortytwo": (java.lang.Long(42), java.lang.Long.TYPE) }
73 | log.debug("Constants: %s", self.constants)
74 | code = self.classfile.addMethod("", ProxyCodeHelpers.makeSig("V"), Modifier.STATIC)
75 | for constant, (value, constant_type) in sorted(self.constants.iteritems()):
76 | self.classfile.addField(
77 | constant,
78 | CodegenUtils.ci(constant_type), Modifier.PUBLIC | Modifier.STATIC | Modifier.FINAL)
79 | code.visitLdcInsn(value)
80 | code.putstatic(self.classfile.name, constant, CodegenUtils.ci(constant_type))
81 | code.return_()
82 |
83 | def saveBytes(self, bytes):
84 | get_builder().write_class_bytes(self.package, self.myClass, bytes)
85 |
86 | def makeClass(self):
87 | builder = get_builder()
88 | log.debug("Entering makeClass for %r", self)
89 | try:
90 | import sys
91 | log.debug("Current sys.path: %s", sys.path)
92 | # If already defined on sys.path (including CLASSPATH), simply return this class
93 | # if you need to tune this, derive accordingly from this class or create another CustomMaker
94 | cls = Py.findClass(self.myClass)
95 | log.debug("Looked up proxy: %r, %r", self.myClass, cls)
96 | if cls is None:
97 | raise TypeError("No proxy class")
98 | except:
99 | if builder:
100 | log.debug("Calling super... for %r", self.package)
101 | cls = CustomMaker.makeClass(self)
102 | log.info("Built proxy: %r", self.myClass)
103 | else:
104 | raise TypeError("Cannot clamp proxy class {} without a defined builder".format(self.myClass))
105 | return cls
106 |
107 |
108 | class ClampProxyMaker(object):
109 |
110 | def __init__(self, package, **kwargs):
111 | self.package = package
112 | self.kwargs = kwargs
113 |
114 | def __call__(self, superclass, interfaces, className, pythonModuleName, fullProxyName, mapping):
115 | """Constructs a usable proxy name that does not depend on ordering"""
116 | log.debug("Called ClampProxyMaker: %s, %r, %r, %s, %s, %s, %r", self.package, superclass, interfaces,
117 | className, pythonModuleName, fullProxyName, mapping)
118 | return SerializableProxyMaker(
119 | superclass, interfaces, className, pythonModuleName,
120 | self.package + "." + pythonModuleName + "." + className, mapping,
121 | self.package, self.kwargs)
122 |
--------------------------------------------------------------------------------
/clamp/signature.py:
--------------------------------------------------------------------------------
1 | class Constant(object):
2 | """ Use this class to declare class attributes as java const
3 |
4 | Example::
5 |
6 | class Test(BarClamp, Callable, Serializable):
7 |
8 | serialVersionUID = Constant(Long(1234), Long.TYPE)
9 | """
10 |
11 | def __init__(self, value, type=None):
12 | # FIXME do type inference on value_type when we have that
13 | if type is None:
14 | raise NotImplementedError("type has to be set right now")
15 | self.value = value
16 | self.type = type
17 |
--------------------------------------------------------------------------------
/ez_setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Bootstrap setuptools installation
3 |
4 | To use setuptools in your package's setup.py, include this
5 | file in the same directory and add this to the top of your setup.py::
6 |
7 | from ez_setup import use_setuptools
8 | use_setuptools()
9 |
10 | To require a specific version of setuptools, set a download
11 | mirror, or use an alternate download directory, simply supply
12 | the appropriate options to ``use_setuptools()``.
13 |
14 | This file can also be run as a script to install or upgrade setuptools.
15 | """
16 | import os
17 | import shutil
18 | import sys
19 | import tempfile
20 | import tarfile
21 | import optparse
22 | import subprocess
23 | import platform
24 | import textwrap
25 |
26 | from distutils import log
27 |
28 | try:
29 | from site import USER_SITE
30 | except ImportError:
31 | USER_SITE = None
32 |
33 | DEFAULT_VERSION = "2.1"
34 | DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
35 |
36 | def _python_cmd(*args):
37 | args = (sys.executable,) + args
38 | return subprocess.call(args) == 0
39 |
40 | def _install(tarball, install_args=()):
41 | # extracting the tarball
42 | tmpdir = tempfile.mkdtemp()
43 | log.warn('Extracting in %s', tmpdir)
44 | old_wd = os.getcwd()
45 | try:
46 | os.chdir(tmpdir)
47 | tar = tarfile.open(tarball)
48 | _extractall(tar)
49 | tar.close()
50 |
51 | # going in the directory
52 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
53 | os.chdir(subdir)
54 | log.warn('Now working in %s', subdir)
55 |
56 | # installing
57 | log.warn('Installing Setuptools')
58 | if not _python_cmd('setup.py', 'install', *install_args):
59 | log.warn('Something went wrong during the installation.')
60 | log.warn('See the error message above.')
61 | # exitcode will be 2
62 | return 2
63 | finally:
64 | os.chdir(old_wd)
65 | shutil.rmtree(tmpdir)
66 |
67 |
68 | def _build_egg(egg, tarball, to_dir):
69 | # extracting the tarball
70 | tmpdir = tempfile.mkdtemp()
71 | log.warn('Extracting in %s', tmpdir)
72 | old_wd = os.getcwd()
73 | try:
74 | os.chdir(tmpdir)
75 | tar = tarfile.open(tarball)
76 | _extractall(tar)
77 | tar.close()
78 |
79 | # going in the directory
80 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
81 | os.chdir(subdir)
82 | log.warn('Now working in %s', subdir)
83 |
84 | # building an egg
85 | log.warn('Building a Setuptools egg in %s', to_dir)
86 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
87 |
88 | finally:
89 | os.chdir(old_wd)
90 | shutil.rmtree(tmpdir)
91 | # returning the result
92 | log.warn(egg)
93 | if not os.path.exists(egg):
94 | raise IOError('Could not build the egg.')
95 |
96 |
97 | def _do_download(version, download_base, to_dir, download_delay):
98 | egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
99 | % (version, sys.version_info[0], sys.version_info[1]))
100 | if not os.path.exists(egg):
101 | tarball = download_setuptools(version, download_base,
102 | to_dir, download_delay)
103 | _build_egg(egg, tarball, to_dir)
104 | sys.path.insert(0, egg)
105 |
106 | # Remove previously-imported pkg_resources if present (see
107 | # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
108 | if 'pkg_resources' in sys.modules:
109 | del sys.modules['pkg_resources']
110 |
111 | import setuptools
112 | setuptools.bootstrap_install_from = egg
113 |
114 |
115 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
116 | to_dir=os.curdir, download_delay=15):
117 | to_dir = os.path.abspath(to_dir)
118 | rep_modules = 'pkg_resources', 'setuptools'
119 | imported = set(sys.modules).intersection(rep_modules)
120 | try:
121 | import pkg_resources
122 | except ImportError:
123 | return _do_download(version, download_base, to_dir, download_delay)
124 | try:
125 | pkg_resources.require("setuptools>=" + version)
126 | return
127 | except pkg_resources.DistributionNotFound:
128 | return _do_download(version, download_base, to_dir, download_delay)
129 | except pkg_resources.VersionConflict as VC_err:
130 | if imported:
131 | msg = textwrap.dedent("""
132 | The required version of setuptools (>={version}) is not available,
133 | and can't be installed while this script is running. Please
134 | install a more recent version first, using
135 | 'easy_install -U setuptools'.
136 |
137 | (Currently using {VC_err.args[0]!r})
138 | """).format(VC_err=VC_err, version=version)
139 | sys.stderr.write(msg)
140 | sys.exit(2)
141 |
142 | # otherwise, reload ok
143 | del pkg_resources, sys.modules['pkg_resources']
144 | return _do_download(version, download_base, to_dir, download_delay)
145 |
146 | def _clean_check(cmd, target):
147 | """
148 | Run the command to download target. If the command fails, clean up before
149 | re-raising the error.
150 | """
151 | try:
152 | subprocess.check_call(cmd)
153 | except subprocess.CalledProcessError:
154 | if os.access(target, os.F_OK):
155 | os.unlink(target)
156 | raise
157 |
158 | def download_file_powershell(url, target):
159 | """
160 | Download the file at url to target using Powershell (which will validate
161 | trust). Raise an exception if the command cannot complete.
162 | """
163 | target = os.path.abspath(target)
164 | cmd = [
165 | 'powershell',
166 | '-Command',
167 | "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(),
168 | ]
169 | _clean_check(cmd, target)
170 |
171 | def has_powershell():
172 | if platform.system() != 'Windows':
173 | return False
174 | cmd = ['powershell', '-Command', 'echo test']
175 | devnull = open(os.path.devnull, 'wb')
176 | try:
177 | try:
178 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
179 | except:
180 | return False
181 | finally:
182 | devnull.close()
183 | return True
184 |
185 | download_file_powershell.viable = has_powershell
186 |
187 | def download_file_curl(url, target):
188 | cmd = ['curl', url, '--silent', '--output', target]
189 | _clean_check(cmd, target)
190 |
191 | def has_curl():
192 | cmd = ['curl', '--version']
193 | devnull = open(os.path.devnull, 'wb')
194 | try:
195 | try:
196 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
197 | except:
198 | return False
199 | finally:
200 | devnull.close()
201 | return True
202 |
203 | download_file_curl.viable = has_curl
204 |
205 | def download_file_wget(url, target):
206 | cmd = ['wget', url, '--quiet', '--output-document', target]
207 | _clean_check(cmd, target)
208 |
209 | def has_wget():
210 | cmd = ['wget', '--version']
211 | devnull = open(os.path.devnull, 'wb')
212 | try:
213 | try:
214 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
215 | except:
216 | return False
217 | finally:
218 | devnull.close()
219 | return True
220 |
221 | download_file_wget.viable = has_wget
222 |
223 | def download_file_insecure(url, target):
224 | """
225 | Use Python to download the file, even though it cannot authenticate the
226 | connection.
227 | """
228 | try:
229 | from urllib.request import urlopen
230 | except ImportError:
231 | from urllib2 import urlopen
232 | src = dst = None
233 | try:
234 | src = urlopen(url)
235 | # Read/write all in one block, so we don't create a corrupt file
236 | # if the download is interrupted.
237 | data = src.read()
238 | dst = open(target, "wb")
239 | dst.write(data)
240 | finally:
241 | if src:
242 | src.close()
243 | if dst:
244 | dst.close()
245 |
246 | download_file_insecure.viable = lambda: True
247 |
248 | def get_best_downloader():
249 | downloaders = [
250 | download_file_powershell,
251 | download_file_curl,
252 | download_file_wget,
253 | download_file_insecure,
254 | ]
255 |
256 | for dl in downloaders:
257 | if dl.viable():
258 | return dl
259 |
260 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
261 | to_dir=os.curdir, delay=15,
262 | downloader_factory=get_best_downloader):
263 | """Download setuptools from a specified location and return its filename
264 |
265 | `version` should be a valid setuptools version number that is available
266 | as an egg for download under the `download_base` URL (which should end
267 | with a '/'). `to_dir` is the directory where the egg will be downloaded.
268 | `delay` is the number of seconds to pause before an actual download
269 | attempt.
270 |
271 | ``downloader_factory`` should be a function taking no arguments and
272 | returning a function for downloading a URL to a target.
273 | """
274 | # making sure we use the absolute path
275 | to_dir = os.path.abspath(to_dir)
276 | tgz_name = "setuptools-%s.tar.gz" % version
277 | url = download_base + tgz_name
278 | saveto = os.path.join(to_dir, tgz_name)
279 | if not os.path.exists(saveto): # Avoid repeated downloads
280 | log.warn("Downloading %s", url)
281 | downloader = downloader_factory()
282 | downloader(url, saveto)
283 | return os.path.realpath(saveto)
284 |
285 |
286 | def _extractall(self, path=".", members=None):
287 | """Extract all members from the archive to the current working
288 | directory and set owner, modification time and permissions on
289 | directories afterwards. `path' specifies a different directory
290 | to extract to. `members' is optional and must be a subset of the
291 | list returned by getmembers().
292 | """
293 | import copy
294 | import operator
295 | from tarfile import ExtractError
296 | directories = []
297 |
298 | if members is None:
299 | members = self
300 |
301 | for tarinfo in members:
302 | if tarinfo.isdir():
303 | # Extract directories with a safe mode.
304 | directories.append(tarinfo)
305 | tarinfo = copy.copy(tarinfo)
306 | tarinfo.mode = 448 # decimal for oct 0700
307 | self.extract(tarinfo, path)
308 |
309 | # Reverse sort directories.
310 | directories.sort(key=operator.attrgetter('name'), reverse=True)
311 |
312 | # Set correct owner, mtime and filemode on directories.
313 | for tarinfo in directories:
314 | dirpath = os.path.join(path, tarinfo.name)
315 | try:
316 | self.chown(tarinfo, dirpath)
317 | self.utime(tarinfo, dirpath)
318 | self.chmod(tarinfo, dirpath)
319 | except ExtractError as e:
320 | if self.errorlevel > 1:
321 | raise
322 | else:
323 | self._dbg(1, "tarfile: %s" % e)
324 |
325 |
326 | def _build_install_args(options):
327 | """
328 | Build the arguments to 'python setup.py install' on the setuptools package
329 | """
330 | return ['--user'] if options.user_install else []
331 |
332 | def _parse_args():
333 | """
334 | Parse the command line for options
335 | """
336 | parser = optparse.OptionParser()
337 | parser.add_option(
338 | '--user', dest='user_install', action='store_true', default=False,
339 | help='install in user site package (requires Python 2.6 or later)')
340 | parser.add_option(
341 | '--download-base', dest='download_base', metavar="URL",
342 | default=DEFAULT_URL,
343 | help='alternative URL from where to download the setuptools package')
344 | parser.add_option(
345 | '--insecure', dest='downloader_factory', action='store_const',
346 | const=lambda: download_file_insecure, default=get_best_downloader,
347 | help='Use internal, non-validating downloader'
348 | )
349 | options, args = parser.parse_args()
350 | # positional arguments are ignored
351 | return options
352 |
353 | def main(version=DEFAULT_VERSION):
354 | """Install or upgrade setuptools and EasyInstall"""
355 | options = _parse_args()
356 | tarball = download_setuptools(download_base=options.download_base,
357 | downloader_factory=options.downloader_factory)
358 | return _install(tarball, _build_install_args(options))
359 |
360 | if __name__ == '__main__':
361 | sys.exit(main())
362 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import ez_setup
2 | ez_setup.use_setuptools()
3 |
4 | from setuptools import setup, find_packages
5 |
6 |
7 | setup(
8 | name = "clamp",
9 | version = "0.4",
10 | packages = find_packages(),
11 | entry_points = {
12 | "distutils.commands": [
13 | "build_jar = clamp.commands:build_jar_command",
14 | "clamp = clamp.commands:clamp_command",
15 | "singlejar = clamp.commands:singlejar_command",
16 | ],
17 | "distutils.setup_keywords": [
18 | "clamp = clamp.commands:parse_clamp_keyword",
19 | ],
20 | "console_scripts": [
21 | "singlejar = clamp.commands:singlejar_script_command",
22 | ]
23 | },
24 | zip_safe = True
25 | )
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/tests/integ/README.md:
--------------------------------------------------------------------------------
1 | Running tests
2 | =============
3 | Before you run the tests make sure you've built the latest clamp.
4 |
5 | Running tests themselves is really easy:
6 | ````bash
7 | $ cd tests/integ
8 | $ jython27 setup.py test
9 | ````
10 |
11 | The output would look something like this:
12 | ````
13 | running test
14 | running build_jar
15 | Ran 2 tests in 5s, failures: 0
16 | ````
17 |
18 | Developing tests
19 | ================
20 | There are two parts to the tests, the python side to be clamped lives in
21 | `clamp_samples` directory and the JUnit tests live in `junit_tests` directory.
22 |
23 | Essentially, we create some sample Python class, then we test it from the
24 | Java side using JUnit.
25 |
26 | The Python bits
27 | ---------------
28 | * Create a new sample python file which would contain something like below:
29 |
30 | ````python
31 | from java.lang import Long, Object
32 | from clamp import clamp_base, Constant
33 |
34 | TestBase = clamp_base("org")
35 |
36 |
37 | class ConstSample(TestBase, Object):
38 | myConstant = Constant(Long(1234), Long.TYPE)
39 | ````
40 |
41 | * Add an import of it in the `__init__.py` (so that clamp will notice it when "clamping".
42 |
43 | The Java bits
44 | -------------
45 | In the `junit_tests` directory add a new Test[Sample].java file and write a JUnit test(s).
46 |
47 | ````java
48 | import org.junit.Test;
49 | import static org.junit.Assert.*;
50 |
51 | import org.clamp_samples.const.ConstSample;
52 |
53 | public class TestConstant {
54 | @Test
55 | public void testConstant() throws Exception {
56 | ConstTest constObj = new ConstTest();
57 |
58 | assertEquals(1234, constObj.myConstant);
59 | }
60 | }
61 | ````
62 |
--------------------------------------------------------------------------------
/tests/integ/clamp_samples/__init__.py:
--------------------------------------------------------------------------------
1 | from . import callable, const_
--------------------------------------------------------------------------------
/tests/integ/clamp_samples/callable.py:
--------------------------------------------------------------------------------
1 | from java.io import Serializable
2 | from java.util.concurrent import Callable
3 |
4 | from clamp import clamp_base
5 |
6 |
7 | TestBase = clamp_base("org")
8 |
9 |
10 | class CallableSample(TestBase, Callable, Serializable):
11 |
12 | def call(self):
13 | return 42
--------------------------------------------------------------------------------
/tests/integ/clamp_samples/const_.py:
--------------------------------------------------------------------------------
1 | from java.lang import Long, Object
2 | from clamp import clamp_base, Constant
3 |
4 | TestBase = clamp_base("org")
5 |
6 |
7 | class ConstSample(TestBase, Object):
8 | myConstant = Constant(Long(1234), Long.TYPE)
--------------------------------------------------------------------------------
/tests/integ/ez_setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Bootstrap setuptools installation
3 |
4 | To use setuptools in your package's setup.py, include this
5 | file in the same directory and add this to the top of your setup.py::
6 |
7 | from ez_setup import use_setuptools
8 | use_setuptools()
9 |
10 | To require a specific version of setuptools, set a download
11 | mirror, or use an alternate download directory, simply supply
12 | the appropriate options to ``use_setuptools()``.
13 |
14 | This file can also be run as a script to install or upgrade setuptools.
15 | """
16 | import os
17 | import shutil
18 | import sys
19 | import tempfile
20 | import tarfile
21 | import optparse
22 | import subprocess
23 | import platform
24 | import textwrap
25 |
26 | from distutils import log
27 |
28 | try:
29 | from site import USER_SITE
30 | except ImportError:
31 | USER_SITE = None
32 |
33 | DEFAULT_VERSION = "2.1"
34 | DEFAULT_URL = "https://pypi.python.org/packages/source/s/setuptools/"
35 |
36 | def _python_cmd(*args):
37 | args = (sys.executable,) + args
38 | return subprocess.call(args) == 0
39 |
40 | def _install(tarball, install_args=()):
41 | # extracting the tarball
42 | tmpdir = tempfile.mkdtemp()
43 | log.warn('Extracting in %s', tmpdir)
44 | old_wd = os.getcwd()
45 | try:
46 | os.chdir(tmpdir)
47 | tar = tarfile.open(tarball)
48 | _extractall(tar)
49 | tar.close()
50 |
51 | # going in the directory
52 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
53 | os.chdir(subdir)
54 | log.warn('Now working in %s', subdir)
55 |
56 | # installing
57 | log.warn('Installing Setuptools')
58 | if not _python_cmd('setup.py', 'install', *install_args):
59 | log.warn('Something went wrong during the installation.')
60 | log.warn('See the error message above.')
61 | # exitcode will be 2
62 | return 2
63 | finally:
64 | os.chdir(old_wd)
65 | shutil.rmtree(tmpdir)
66 |
67 |
68 | def _build_egg(egg, tarball, to_dir):
69 | # extracting the tarball
70 | tmpdir = tempfile.mkdtemp()
71 | log.warn('Extracting in %s', tmpdir)
72 | old_wd = os.getcwd()
73 | try:
74 | os.chdir(tmpdir)
75 | tar = tarfile.open(tarball)
76 | _extractall(tar)
77 | tar.close()
78 |
79 | # going in the directory
80 | subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0])
81 | os.chdir(subdir)
82 | log.warn('Now working in %s', subdir)
83 |
84 | # building an egg
85 | log.warn('Building a Setuptools egg in %s', to_dir)
86 | _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir)
87 |
88 | finally:
89 | os.chdir(old_wd)
90 | shutil.rmtree(tmpdir)
91 | # returning the result
92 | log.warn(egg)
93 | if not os.path.exists(egg):
94 | raise IOError('Could not build the egg.')
95 |
96 |
97 | def _do_download(version, download_base, to_dir, download_delay):
98 | egg = os.path.join(to_dir, 'setuptools-%s-py%d.%d.egg'
99 | % (version, sys.version_info[0], sys.version_info[1]))
100 | if not os.path.exists(egg):
101 | tarball = download_setuptools(version, download_base,
102 | to_dir, download_delay)
103 | _build_egg(egg, tarball, to_dir)
104 | sys.path.insert(0, egg)
105 |
106 | # Remove previously-imported pkg_resources if present (see
107 | # https://bitbucket.org/pypa/setuptools/pull-request/7/ for details).
108 | if 'pkg_resources' in sys.modules:
109 | del sys.modules['pkg_resources']
110 |
111 | import setuptools
112 | setuptools.bootstrap_install_from = egg
113 |
114 |
115 | def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
116 | to_dir=os.curdir, download_delay=15):
117 | to_dir = os.path.abspath(to_dir)
118 | rep_modules = 'pkg_resources', 'setuptools'
119 | imported = set(sys.modules).intersection(rep_modules)
120 | try:
121 | import pkg_resources
122 | except ImportError:
123 | return _do_download(version, download_base, to_dir, download_delay)
124 | try:
125 | pkg_resources.require("setuptools>=" + version)
126 | return
127 | except pkg_resources.DistributionNotFound:
128 | return _do_download(version, download_base, to_dir, download_delay)
129 | except pkg_resources.VersionConflict as VC_err:
130 | if imported:
131 | msg = textwrap.dedent("""
132 | The required version of setuptools (>={version}) is not available,
133 | and can't be installed while this script is running. Please
134 | install a more recent version first, using
135 | 'easy_install -U setuptools'.
136 |
137 | (Currently using {VC_err.args[0]!r})
138 | """).format(VC_err=VC_err, version=version)
139 | sys.stderr.write(msg)
140 | sys.exit(2)
141 |
142 | # otherwise, reload ok
143 | del pkg_resources, sys.modules['pkg_resources']
144 | return _do_download(version, download_base, to_dir, download_delay)
145 |
146 | def _clean_check(cmd, target):
147 | """
148 | Run the command to download target. If the command fails, clean up before
149 | re-raising the error.
150 | """
151 | try:
152 | subprocess.check_call(cmd)
153 | except subprocess.CalledProcessError:
154 | if os.access(target, os.F_OK):
155 | os.unlink(target)
156 | raise
157 |
158 | def download_file_powershell(url, target):
159 | """
160 | Download the file at url to target using Powershell (which will validate
161 | trust). Raise an exception if the command cannot complete.
162 | """
163 | target = os.path.abspath(target)
164 | cmd = [
165 | 'powershell',
166 | '-Command',
167 | "(new-object System.Net.WebClient).DownloadFile(%(url)r, %(target)r)" % vars(),
168 | ]
169 | _clean_check(cmd, target)
170 |
171 | def has_powershell():
172 | if platform.system() != 'Windows':
173 | return False
174 | cmd = ['powershell', '-Command', 'echo test']
175 | devnull = open(os.path.devnull, 'wb')
176 | try:
177 | try:
178 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
179 | except:
180 | return False
181 | finally:
182 | devnull.close()
183 | return True
184 |
185 | download_file_powershell.viable = has_powershell
186 |
187 | def download_file_curl(url, target):
188 | cmd = ['curl', url, '--silent', '--output', target]
189 | _clean_check(cmd, target)
190 |
191 | def has_curl():
192 | cmd = ['curl', '--version']
193 | devnull = open(os.path.devnull, 'wb')
194 | try:
195 | try:
196 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
197 | except:
198 | return False
199 | finally:
200 | devnull.close()
201 | return True
202 |
203 | download_file_curl.viable = has_curl
204 |
205 | def download_file_wget(url, target):
206 | cmd = ['wget', url, '--quiet', '--output-document', target]
207 | _clean_check(cmd, target)
208 |
209 | def has_wget():
210 | cmd = ['wget', '--version']
211 | devnull = open(os.path.devnull, 'wb')
212 | try:
213 | try:
214 | subprocess.check_call(cmd, stdout=devnull, stderr=devnull)
215 | except:
216 | return False
217 | finally:
218 | devnull.close()
219 | return True
220 |
221 | download_file_wget.viable = has_wget
222 |
223 | def download_file_insecure(url, target):
224 | """
225 | Use Python to download the file, even though it cannot authenticate the
226 | connection.
227 | """
228 | try:
229 | from urllib.request import urlopen
230 | except ImportError:
231 | from urllib2 import urlopen
232 | src = dst = None
233 | try:
234 | src = urlopen(url)
235 | # Read/write all in one block, so we don't create a corrupt file
236 | # if the download is interrupted.
237 | data = src.read()
238 | dst = open(target, "wb")
239 | dst.write(data)
240 | finally:
241 | if src:
242 | src.close()
243 | if dst:
244 | dst.close()
245 |
246 | download_file_insecure.viable = lambda: True
247 |
248 | def get_best_downloader():
249 | downloaders = [
250 | download_file_powershell,
251 | download_file_curl,
252 | download_file_wget,
253 | download_file_insecure,
254 | ]
255 |
256 | for dl in downloaders:
257 | if dl.viable():
258 | return dl
259 |
260 | def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL,
261 | to_dir=os.curdir, delay=15,
262 | downloader_factory=get_best_downloader):
263 | """Download setuptools from a specified location and return its filename
264 |
265 | `version` should be a valid setuptools version number that is available
266 | as an egg for download under the `download_base` URL (which should end
267 | with a '/'). `to_dir` is the directory where the egg will be downloaded.
268 | `delay` is the number of seconds to pause before an actual download
269 | attempt.
270 |
271 | ``downloader_factory`` should be a function taking no arguments and
272 | returning a function for downloading a URL to a target.
273 | """
274 | # making sure we use the absolute path
275 | to_dir = os.path.abspath(to_dir)
276 | tgz_name = "setuptools-%s.tar.gz" % version
277 | url = download_base + tgz_name
278 | saveto = os.path.join(to_dir, tgz_name)
279 | if not os.path.exists(saveto): # Avoid repeated downloads
280 | log.warn("Downloading %s", url)
281 | downloader = downloader_factory()
282 | downloader(url, saveto)
283 | return os.path.realpath(saveto)
284 |
285 |
286 | def _extractall(self, path=".", members=None):
287 | """Extract all members from the archive to the current working
288 | directory and set owner, modification time and permissions on
289 | directories afterwards. `path' specifies a different directory
290 | to extract to. `members' is optional and must be a subset of the
291 | list returned by getmembers().
292 | """
293 | import copy
294 | import operator
295 | from tarfile import ExtractError
296 | directories = []
297 |
298 | if members is None:
299 | members = self
300 |
301 | for tarinfo in members:
302 | if tarinfo.isdir():
303 | # Extract directories with a safe mode.
304 | directories.append(tarinfo)
305 | tarinfo = copy.copy(tarinfo)
306 | tarinfo.mode = 448 # decimal for oct 0700
307 | self.extract(tarinfo, path)
308 |
309 | # Reverse sort directories.
310 | directories.sort(key=operator.attrgetter('name'), reverse=True)
311 |
312 | # Set correct owner, mtime and filemode on directories.
313 | for tarinfo in directories:
314 | dirpath = os.path.join(path, tarinfo.name)
315 | try:
316 | self.chown(tarinfo, dirpath)
317 | self.utime(tarinfo, dirpath)
318 | self.chmod(tarinfo, dirpath)
319 | except ExtractError as e:
320 | if self.errorlevel > 1:
321 | raise
322 | else:
323 | self._dbg(1, "tarfile: %s" % e)
324 |
325 |
326 | def _build_install_args(options):
327 | """
328 | Build the arguments to 'python setup.py install' on the setuptools package
329 | """
330 | return ['--user'] if options.user_install else []
331 |
332 | def _parse_args():
333 | """
334 | Parse the command line for options
335 | """
336 | parser = optparse.OptionParser()
337 | parser.add_option(
338 | '--user', dest='user_install', action='store_true', default=False,
339 | help='install in user site package (requires Python 2.6 or later)')
340 | parser.add_option(
341 | '--download-base', dest='download_base', metavar="URL",
342 | default=DEFAULT_URL,
343 | help='alternative URL from where to download the setuptools package')
344 | parser.add_option(
345 | '--insecure', dest='downloader_factory', action='store_const',
346 | const=lambda: download_file_insecure, default=get_best_downloader,
347 | help='Use internal, non-validating downloader'
348 | )
349 | options, args = parser.parse_args()
350 | # positional arguments are ignored
351 | return options
352 |
353 | def main(version=DEFAULT_VERSION):
354 | """Install or upgrade setuptools and EasyInstall"""
355 | options = _parse_args()
356 | tarball = download_setuptools(download_base=options.download_base,
357 | downloader_factory=options.downloader_factory)
358 | return _install(tarball, _build_install_args(options))
359 |
360 | if __name__ == '__main__':
361 | sys.exit(main())
362 |
--------------------------------------------------------------------------------
/tests/integ/junit_tests/TestCallable.java:
--------------------------------------------------------------------------------
1 | import org.junit.Test;
2 | import static org.junit.Assert.*;
3 |
4 | import org.clamp_samples.callable.CallableSample;
5 |
6 | public class TestCallable {
7 | @Test
8 | public void testCallable() throws Exception {
9 | CallableSample callable = new CallableSample();
10 |
11 | assertEquals(42, callable.call());
12 | }
13 | }
--------------------------------------------------------------------------------
/tests/integ/junit_tests/TestConstant.java:
--------------------------------------------------------------------------------
1 | import org.junit.Test;
2 | import static org.junit.Assert.*;
3 |
4 | import org.clamp_samples.const_.ConstSample;
5 |
6 | public class TestConstant {
7 | @Test
8 | public void testConstant() throws Exception {
9 | ConstSample constObj = new ConstSample();
10 |
11 | assertEquals(1234, constObj.myConstant);
12 | }
13 | }
--------------------------------------------------------------------------------
/tests/integ/setup.py:
--------------------------------------------------------------------------------
1 | import ez_setup
2 | ez_setup.use_setuptools()
3 |
4 | import sys
5 | import os
6 |
7 | from setuptools import setup, find_packages, Command
8 | from glob import glob
9 |
10 | # add parent clamp path
11 | sys.path.append("../../")
12 |
13 | from clamp.commands import clamp_command
14 |
15 |
16 | from org.junit.runner import JUnitCore
17 | from javax.tools import ToolProvider
18 | from java.net import URLClassLoader
19 | from java.net import URL
20 |
21 |
22 | class test_command(Command):
23 |
24 | description = "Run junit tests"
25 | user_options = [
26 | ("tempdir=", "t", "temporary directory for test data"),
27 | ("junit-testdir=", "j", "directory containing junit tests")
28 | ]
29 |
30 | def initialize_options(self):
31 | self.tempdir = 'build/tmp'
32 | self.junit_testdir = 'junit_tests'
33 |
34 | def finalize_options(self):
35 | self.testjar = os.path.join(self.tempdir, 'tests.jar')
36 | self.test_classesdir = os.path.join(self.tempdir, 'classes')
37 |
38 | def build_jar(self):
39 | build_jar_cmd = self.distribution.get_command_obj('build_jar')
40 | build_jar_cmd.output = os.path.join(self.testjar)
41 | self.run_command('build_jar')
42 |
43 | def get_classpath(self):
44 | jython_dir = os.path.split(os.path.split(sys.executable)[0])[0]
45 | junit = glob(os.path.join(jython_dir, 'javalib/junit-*.jar'))[0]
46 |
47 | return ":".join([os.path.join(jython_dir, sys.JYTHON_DEV_JAR),
48 | junit, self.testjar])
49 |
50 | def get_test_classes(self):
51 | urls = [URL('file:' + os.path.abspath(self.test_classesdir) + '/'),
52 | URL('file:' + os.path.abspath(self.testjar))]
53 | urls.extend(URLClassLoader.getSystemClassLoader().getURLs())
54 | loader = URLClassLoader(urls)
55 |
56 | for fname in [name for name in self.get_java_files() if 'test' in name.lower()]:
57 | classname = os.path.splitext(fname)[0]
58 | yield loader.loadClass(classname)
59 |
60 | def get_java_files(self):
61 | return [fname for fname in os.listdir(self.junit_testdir) if fname.endswith('java')]
62 |
63 | def run_javac(self):
64 | javac = ToolProvider.getSystemJavaCompiler()
65 | for fname in self.get_java_files():
66 | err = javac.run(None, None, None, ['-cp', self.get_classpath(), '-d', self.test_classesdir,
67 | os.path.join(self.junit_testdir, fname)])
68 | if err:
69 | sys.exit()
70 |
71 | def run_junit(self):
72 | result = JUnitCore.runClasses(list(self.get_test_classes()))
73 | print "Ran {} tests in {}s, failures: {}".format(result.runCount, result.runTime, result.failureCount)
74 |
75 | if result.failures:
76 | print "Failures:"
77 | for failure in result.failures:
78 | print failure
79 |
80 | def run(self):
81 | self.mkpath(os.path.join(self.tempdir, 'classes'))
82 | self.build_jar()
83 | self.run_javac()
84 | self.run_junit()
85 |
86 |
87 | setup(
88 | name = "clamp-tests",
89 | version = "0.1",
90 | packages = find_packages(),
91 | clamp = {
92 | "modules": ["clamp_samples"]
93 | },
94 | cmdclass = { "install": clamp_command,
95 | "test": test_command}
96 | )
97 |
98 |
--------------------------------------------------------------------------------