Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support to assign customized file extension #115

Merged
merged 7 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Pyconcrete Changelog

## 1.0.1 (2025-??-??)

### Features
* Support to assign customized file extension. Which default is .pye https://github.com/Falldog/pyconcrete/pull/115

### Bug fixes
* Fix return code should not be 0 when python script is exception https://github.com/Falldog/pyconcrete/pull/114



## 1.0.0 (2025-02-23)

### Features
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ help:
@printf "install {ENV: PASSPHRASE=}\n"
@printf "testpypi-install {ENV: VERSION= PASSPHRASE=}\n"


clean:
find . -type f -name "*.pyc" -exec rm -f {} \;

test:
VER="$(filter-out $@,$(MAKECMDGOALS))"; \
if [ -z "$$VER" ]; then \
Expand All @@ -26,7 +30,7 @@ test:

test-all :
for ver in $(PY_VERSIONS); do \
PY_VER=$$ver ./bin/run-test.sh; \
PY_VER=$$ver ./bin/run-test.sh || exit 1; \
done

attach:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ $ pip install pyconcrete \
--config-settings=setup-args="-Dpassphrase=<Your_Passphrase>"
```

* Available arguments. Setup by `--config-settings=setup-args="-D<argurment_name>=<value>"`
* `passphrase`: (Mandatory) To generate secret key for encryption.
* `ext`: Able to assign customized encrypted file extension. Which default is `.pye`.

Usage
--------------

Expand All @@ -73,6 +77,7 @@ Usage
```sh
$ pyecli compile --pye -s=<your py script>
$ pyecli compile --pye -s=<your py module dir>
$ pyecli compile --pye -s=<your py module dir> -e=<your file extension>
```

* remove `*.py` `*.pyc` or copy `*.pye` to other folder
Expand Down
10 changes: 9 additions & 1 deletion meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ project(
'c',
meson_version: '>= 1.1',
version: files('VERSION'),
default_options: ['ext=.pye'],
)

INSTALL_DIR = 'pyconcrete'
Expand All @@ -18,6 +19,9 @@ python_ver = python_ins.language_version()
python_exe_path = python_ins.full_path()
python_dependency = python_ins.dependency(embed: true)

macro_ext = '-D PYCONCRETE_EXT="@0@"'.format(get_option('ext'))
macro_version = '-D PYCONCRETE_VERSION=@0@'.format(version)


#----------------------------
# secret_key.h
Expand Down Expand Up @@ -54,6 +58,7 @@ ext_srcs = [
python_ins.extension_module(
'_pyconcrete',
ext_srcs,
c_args: [macro_ext],
subdir: INSTALL_DIR,
install: true,
include_directories: ext_includes,
Expand Down Expand Up @@ -94,7 +99,10 @@ exe_srcs = [
executable(
PYCONCRETE_EXE_NAME,
exe_srcs,
c_args: '-D PYCONCRETE_VERSION=@0@'.format(version),
c_args: [
macro_ext,
macro_version,
],
include_directories: exe_includes,
dependencies: python_dependency,
install: true,
Expand Down
1 change: 1 addition & 0 deletions meson.options
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
option('passphrase', type: 'string', description: 'User-defined passphrase for secret key')
option('ext', type: 'string', description: 'User-defined file extension, default is .pye')
11 changes: 7 additions & 4 deletions pyecli
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import logging
import os
import py_compile
import sys
from os.path import abspath, dirname, exists, isdir, isfile, join
from os.path import abspath, dirname, exists, isdir, isfile, join, splitext

CUR_DIR = dirname(abspath(__file__))

Expand Down Expand Up @@ -63,6 +63,7 @@ class PyConcreteCli(object):
)
parser_compile.add_argument('--pye', dest='pye', action='store_true', help='process on .pye')
parser_compile.add_argument('--pyc', dest='pyc', action='store_true', help='process on .pyc')
parser_compile.add_argument('-e', '--ext', dest='ext', default='.pye', help='file extension, default is .pye')
parser_compile.add_argument(
'--remove-py', dest='remove_py', action='store_true', help='remove .py after compile pye'
)
Expand Down Expand Up @@ -139,7 +140,8 @@ class PyConcreteCli(object):
self._compile_pyc_file(args, fullpath)

def _compile_pyc_file(self, args, py_file):
pyc_file = py_file + 'c'
filename, ext = splitext(py_file)
pyc_file = filename + '.pyc'
pyc_exists = exists(pyc_file)
if not pyc_exists or os.stat(py_file).st_mtime != os.stat(pyc_file).st_mtime:
py_compile.compile(py_file, cfile=pyc_file)
Expand All @@ -159,8 +161,9 @@ class PyConcreteCli(object):
"""
import pyconcrete

pyc_file = py_file + 'c'
pye_file = py_file + 'e'
filename, ext = splitext(py_file)
pyc_file = filename + '.pyc'
pye_file = filename + args.ext
pyc_exists = exists(pyc_file)
if not pyc_exists or os.stat(py_file).st_mtime != os.stat(pyc_file).st_mtime:
py_compile.compile(py_file, cfile=pyc_file)
Expand Down
29 changes: 10 additions & 19 deletions src/pyconcrete/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,34 @@
from importlib._bootstrap_external import _get_supported_file_loaders
from importlib.machinery import SOURCE_SUFFIXES, FileFinder, SourceFileLoader

EXT_PYE = '.pye'
from . import _pyconcrete # noqa: E402

__all__ = ["info"]

_pyconcrete_module = None


def import_pyconcrete_pyd():
"""delay import _pyconcrete for testing"""
global info, _pyconcrete_module
if _pyconcrete_module:
return

from . import _pyconcrete # noqa: E402

_pyconcrete_module = _pyconcrete
_pyconcrete_module = _pyconcrete


def decrypt_buffer(data):
import_pyconcrete_pyd()
return _pyconcrete_module.decrypt_buffer(data)


def encrypt_file(pyc_filepath, pye_filepath):
import_pyconcrete_pyd()
return _pyconcrete_module.encrypt_file(pyc_filepath, pye_filepath)


def info():
import_pyconcrete_pyd()
return _pyconcrete_module.info()


def get_ext():
"""get supported file extension, default should be .pye"""
return _pyconcrete_module.get_ext()


# We need to modify SOURCE_SUFFIXES, because it used in importlib.machinery.all_suffixes function which
# called by inspect.getmodulename and we need to be able to detect the module name relative to .pye files
# because .py can be deleted by us
SOURCE_SUFFIXES.append(EXT_PYE)
SOURCE_SUFFIXES.append(get_ext())


class PyeLoader(SourceFileLoader):
Expand Down Expand Up @@ -92,7 +83,7 @@ def _validate_version(data):
raise ValueError("Python version doesn't match with magic: python(%d) != pye(%d)" % (py_magic, pye_magic))

def get_code(self, fullname):
if not self.path.endswith(EXT_PYE):
if not self.path.endswith(get_ext()):
return super().get_code(fullname)

path = self.get_filename(fullname)
Expand All @@ -101,7 +92,7 @@ def get_code(self, fullname):
return marshal.loads(data[self.magic :])

def get_source(self, fullname):
if self.path.endswith(EXT_PYE):
if self.path.endswith(get_ext()):
return None
return super().get_source(fullname)

Expand Down
10 changes: 10 additions & 0 deletions src/pyconcrete_ext/pyconcrete_module.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,23 @@
#define IS_PY3K
#endif

#ifndef PYCONCRETE_EXT
#define PYCONCRETE_EXT ".pye"
#endif

static PyObject * fnInfo(PyObject *self, PyObject* null)
{
return Py_BuildValue("s", "PyConcrete Info() AES 128bit");
}

static PyObject * fnExt(PyObject *self, PyObject* null)
{
return Py_BuildValue("s", PYCONCRETE_EXT);
}

static PyMethodDef PyConcreteMethods[] = {
{"info", fnInfo, METH_NOARGS, "Display PyConcrete info"},
{"get_ext", fnExt, METH_NOARGS, "PyConcrete file ext"},
{"encrypt_file", fnEncryptFile, METH_VARARGS, "Encrypt whole file"},
{"decrypt_file", fnDecryptFile, METH_VARARGS, "Decrypt whole file (not ready)"},
{"decrypt_buffer", fnDecryptBuffer, METH_VARARGS, "Decrypt buffer"},
Expand Down
46 changes: 25 additions & 21 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import subprocess
import sys
from os.path import abspath, dirname, join
Expand All @@ -23,20 +22,18 @@


class Venv:
def __init__(self, env_dir):
def __init__(self, env_dir, pyconcrete_ext=None):
self.executable = None
self.bin_dir = None
self.env_dir = env_dir
self._pyconcrete_ext = pyconcrete_ext
self.create()

def create(self):
subprocess.check_call([sys.executable, '-m', 'virtualenv', self.env_dir])
self.bin_dir = join(self.env_dir, 'bin')
self.executable = join(self.bin_dir, 'python')

def raise_if_pyconcrete_not_installed(self):
if not os.path.exists(join(self.bin_dir, 'pyconcrete')):
raise Exception("pyconcrete not been installed yet, please make sure you setup pye_cli before use the venv")
self._ensure_pyconcrete_exist()

def python(self, *args: [str]):
return subprocess.check_output([self.executable, *args]).decode()
Expand All @@ -46,26 +43,39 @@ def pip(self, *args: [str]):

@property
def pyconcrete_exe(self):
self.raise_if_pyconcrete_not_installed()
self._ensure_pyconcrete_exist()
return join(self.bin_dir, 'pyconcrete')

def pyconcrete(self, *args: [str]):
self.raise_if_pyconcrete_not_installed()
self._ensure_pyconcrete_exist()
return subprocess.check_output([self.pyconcrete_exe, *args]).decode()

def pyconcrete_cli(self, *args: [str]):
self.raise_if_pyconcrete_not_installed()
self._ensure_pyconcrete_exist()
cli_script = join(ROOT_DIR, 'pyecli')
return subprocess.check_output([self.executable, cli_script, *args]).decode()

def _ensure_pyconcrete_exist(self):
proc = subprocess.run(f'{self.executable} -m pip list | grep -c pyconcrete', shell=True)
pyconcrete_exist = bool(proc.returncode == 0)
if not pyconcrete_exist:
args = [
'install',
f'--config-settings=setup-args=-Dpassphrase={PASSPHRASE}',
f'--config-settings=setup-args=-Dext={self._pyconcrete_ext}' if self._pyconcrete_ext else '',
'--quiet',
ROOT_DIR,
]
args = [arg for arg in args if arg] # filter empty string
self.pip(*args)


class PyeCli:
def __init__(self, venv: Venv):
self._venv = venv
self._tmp_dir = None
self._module_name = None
self._source_code = None
self._ensure_pyconcrete_exist()

def setup(self, tmp_dir, module_name):
self._tmp_dir = tmp_dir
Expand All @@ -77,17 +87,6 @@ def tmp_dir(self):
"""tmp dir to do the encryption"""
return self._tmp_dir

def _ensure_pyconcrete_exist(self):
proc = subprocess.run(f'{self._venv.executable} -m pip list | grep -c pyconcrete', shell=True)
pyconcrete_exist = bool(proc.returncode == 0)
if not pyconcrete_exist:
self._venv.pip(
'install',
f'--config-settings=setup-args=-Dpassphrase={PASSPHRASE}',
'--quiet',
ROOT_DIR,
)

def source_code(self, code):
self._source_code = code
return self
Expand Down Expand Up @@ -119,3 +118,8 @@ def venv(tmp_path_factory):
@pytest.fixture
def sample_module_path():
return join(ROOT_DIR, 'tests', 'fixtures', 'sample_module')


@pytest.fixture
def sample_import_sub_module_path():
return join(ROOT_DIR, 'tests', 'exe_testcases', 'test_import_sub_module')
50 changes: 50 additions & 0 deletions tests/test_customize_ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2015 Falldog Hsieh <[email protected]>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import shutil
from os.path import exists, join

import pytest

from .conftest import Venv


@pytest.mark.parametrize(
"ext",
[".pye", ".t", ".tw"],
)
def test_customize_ext(tmp_path_factory, tmpdir, sample_import_sub_module_path, ext):
"""build the standalone virtualenv for different file extensions"""
# prepare
target_dir = join(tmpdir, 'for_ext_module')
main_encrypted = join(target_dir, f'main{ext}')
venv = Venv(tmp_path_factory.mktemp('venv_ext_'), pyconcrete_ext=ext)

# compile to customized extension
shutil.copytree(sample_import_sub_module_path, target_dir)
venv.pyconcrete_cli(
'compile',
f'--ext={ext}',
f'--source={target_dir}',
'--pye',
'--remove-py',
)

# verification (before)
assert exists(main_encrypted) is True

# execution
output = venv.pyconcrete(main_encrypted)

# verification (after)
assert output == 'bar\n'
2 changes: 1 addition & 1 deletion tests/test_exe.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import subprocess


def test_exe__execute_an_non_exist_file(venv, pye_cli):
def test_exe__execute_an_non_exist_file(venv):
return_code = subprocess.call([venv.pyconcrete_exe, 'non_existing_file.txt'])
assert return_code == 1

Expand Down
Loading