├── .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 | --------------------------------------------------------------------------------