Skip to content

Commit 9b3affe

Browse files
author
Oleg Avdeev
committed
initial import
0 parents  commit 9b3affe

File tree

12 files changed

+1026
-0
lines changed

12 files changed

+1026
-0
lines changed

README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# plunger
2+
3+
Plunger is a python linter, with bult-in [Luigi](https://github.com/spotify/luigi) support.
4+
5+
## Rationale
6+
7+
Existing Python linters and type checkers ([pylint](https://www.pylint.org/), [mypy](http://mypy-lang.org/)) are great, but have their limitations: you can only get so far when dealing with Python code that uses run-time metaprogramming magic. Unfortunately Luigi relies on this quite a bit, that makes generic linters less useful.
8+
9+
For example, they can't effectively detect errors related to Luigi task parameters when they are used as member variables or arguments to the constructor. `pylint` can detect undefined variables only when they are normal local variables, not class members -- most of the time it has no idea if `self.foo` exists or not.
10+
11+
## Solution
12+
13+
These limitations are mostly due to highly dynamic nature of Python, you can't really get around this in a generic way. Plunger's answer is to just give up on that and treat Luigi tasks specially. Plunger only supports a certain well behaved subset of Python. Features like `import *` or inheritance (with the exception of inheriting from luigi.Task) are not supported, but you shouldnt use them anyway.
14+
15+
## Installation
16+
17+
Get [Stack](https://github.com/commercialhaskell/stack/releases)
18+
```
19+
stack build plunger
20+
stack install plunger
21+
```
22+
23+
You should get `plunger` executable under `~/.local/bin`.
24+
25+
## Usage
26+
27+
```
28+
plunger path/to/module.py [path/to/imported.py ...]
29+
```
30+
31+
Where first path is to the module to be checked, and the are paths to imported modules to be checked as well.
32+
33+
`plunger` has only limited support for imports, so it will only process the imported modules that you specify on the command line, the rest will be ignored. Only `from X import Y` imports are supported at the moment.
34+
35+
To illustrate what this means in practice, consider two python modules:
36+
37+
##### otherjobs.py
38+
```python
39+
import luigi
40+
41+
class TestTask(luigi.Task):
42+
foo = luigi.Parameter()
43+
bar = luigi.Parameter()
44+
45+
def run(self):
46+
pass
47+
```
48+
49+
50+
##### myjobs.py
51+
```python
52+
import luigi
53+
from otherjobs import TestTask
54+
55+
class OtherTestTask(luigi.Task):
56+
57+
def requires(self):
58+
return TestTask(foo="a") # missing argument for "bar" here
59+
60+
def run(self):
61+
pass
62+
```
63+
64+
You'll get no warnings if you run `plunger` on **myjobs.py** only:
65+
```shell
66+
$ plunger myjobs.py
67+
No warnings
68+
```
69+
70+
Since `plunger` does not automatically process all imports, it has no idea what the signature of `TestTask` is, and cannot detect the error (missing parameter).
71+
72+
But if you specify path to the other module on the command line, it will parse it as well and will be able to verify the invocation of `TestTask` constructor:
73+
```shell
74+
plunger myjobs.py otherjobs.py
75+
=========== 1 WARNINGS ========
76+
77+
Missing argument bar at row 8 column 16
78+
Expected: foo:AnyType, bar:AnyType
79+
```
80+
81+
# Examples of errors detected by Plunger
82+
83+
#### Undefined class members
84+
85+
```python
86+
from __future__ import print_function
87+
import luigi
88+
89+
class TestTask(luigi.Task):
90+
foo = luigi.Parameter()
91+
bar = luigi.Parameter()
92+
93+
def run(self):
94+
print(self.y)
95+
```
96+
97+
Result:
98+
99+
```
100+
=========== 1 WARNINGS ========
101+
102+
Undefined y at row 10 column 20
103+
```
104+
105+
#### Missing task constructor parameters
106+
107+
```python
108+
from __future__ import print_function
109+
import luigi
110+
111+
class TestTask(luigi.Task):
112+
foo = luigi.Parameter()
113+
bar = luigi.Parameter()
114+
115+
def run(self):
116+
pass
117+
118+
class OtherTestTask(luigi.Task):
119+
def requires(self):
120+
return TestTask(foo="a")
121+
122+
def run(self):
123+
pass
124+
```
125+
126+
Result:
127+
128+
```
129+
=========== 1 WARNINGS ========
130+
131+
Missing argument bar at row 16 column 16
132+
Expected: foo:AnyType, bar:AnyType
133+
```
134+

Setup.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import Distribution.Simple
2+
main = defaultMain

app/Main.hs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Main where
2+
3+
import System.Environment (getArgs)
4+
import Plunger (plunge)
5+
import System.FilePath.Posix (takeBaseName)
6+
import qualified Data.Map.Strict as Map
7+
8+
readModules :: [String] -> IO (Map.Map String String)
9+
readModules filenames = do
10+
sources <- mapM (\x -> readFile x) filenames
11+
return $ Map.fromList $ zip (map takeBaseName filenames) sources
12+
13+
main :: IO ()
14+
main = do
15+
args <- getArgs
16+
case args of
17+
[] -> putStrLn "Usage: plunger path/to/module.py"
18+
filenames -> do
19+
modules <- readModules filenames
20+
modname <- return $ takeBaseName $ head filenames
21+
plunge modules modname

plunger.cabal

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: plunger
2+
version: 0.1.0.0
3+
-- synopsis:
4+
-- description:
5+
homepage: https://github.com/adroll/plunger
6+
license: BSD3
7+
license-file: LICENSE
8+
author: AdRoll Inc
9+
maintainer: [email protected]
10+
copyright: 2017 AdRoll Inc.
11+
category: Web
12+
build-type: Simple
13+
extra-source-files: README.md
14+
cabal-version: >=1.10
15+
16+
library
17+
hs-source-dirs: src
18+
exposed-modules: Plunger
19+
build-depends: base >= 4.7 && < 5
20+
, language-python
21+
, filepath >= 1.4.1.1
22+
, containers
23+
, ansi-terminal
24+
default-language: Haskell2010
25+
26+
executable plunger
27+
hs-source-dirs: app
28+
main-is: Main.hs
29+
ghc-options: -threaded -rtsopts -with-rtsopts=-N
30+
build-depends: base
31+
, plunger
32+
, filepath >= 1.4.1.1
33+
, containers
34+
default-language: Haskell2010
35+
36+
test-suite plunger-test
37+
type: exitcode-stdio-1.0
38+
hs-source-dirs: test
39+
main-is: Spec.hs
40+
build-depends: base
41+
, plunger
42+
ghc-options: -threaded -rtsopts -with-rtsopts=-N
43+
default-language: Haskell2010
44+
45+
source-repository head
46+
type: git
47+
location: https://github.com/adroll/plunger

0 commit comments

Comments
 (0)