Rahul  Hickle

Rahul Hickle

1598376353

TOP 4 APPs para PROGRAMAR no ANDROID

Usar um dispositivo Android para programar ou criar aplicativos Mobile e aplica莽玫es Web 茅 totalmente poss铆vel. Nesse v铆deo apresento 4 APPs que voc锚 pode instalar no seu Smartphone ou Tablet para transforma-lo num verdadeiro Ambiente de Desenvolvimento

What is GEEK

Buddha Community

TOP 4 APPs para PROGRAMAR no ANDROID

Build an Android application with Kivy Python framework

If you鈥檙e a Python developer thinking about getting started with mobile development, then the Kivy framework is your best bet. With Kivy, you can develop platform-independent applications that compile for iOS, Android, Windows, macOS, and Linux. In this article, we鈥檒l cover Android specifically because it is the most used.

We鈥檒l build a simple random number generator app that you can install on your phone and test when you are done. To follow along with this article, you should be familiar with Python. Let鈥檚 get started!

Getting started with Kivy

First, you鈥檒l need a new directory for your app. Make sure you have Python installed on your machine and open a new Python file. You鈥檒l need to install the Kivy module from your terminal using either of the commands below. To avoid any package conflicts, be sure you鈥檙e installing Kivy in a virtual environment:

pip install kivy 
//
pip3 install kivy 

Once you have installed Kivy, you should see a success message from your terminal that looks like the screenshots below:

Kivy installation

Successful Kivy installation

 

Next, navigate into your project folder. In the main.py file, we鈥檒l need to import the Kivy module and specify which version we want. You can use Kivy v2.0.0, but if you have a smartphone that is older than Android 8.0, I recommend using Kivy v1.9.0. You can mess around with the different versions during the build to see the differences in features and performance.

Add the version number right after the import kivy line as follows:

kivy.require('1.9.0')

Now, we鈥檒l create a class that will basically define our app; I鈥檒l name mine RandomNumber. This class will inherit the app class from Kivy. Therefore, you need to import the app by adding from kivy.app import App:

class RandomNumber(App): 

In the RandomNumber class, you鈥檒l need to add a function called build, which takes a self parameter. To actually return the UI, we鈥檒l use the build function. For now, I have it returned as a simple label. To do so, you鈥檒l need to import Label using the line from kivy.uix.label import Label:

import kivy
from kivy.app import App
from kivy.uix.label import Label

class RandomNumber(App):
  def build(self):
    return Label(text="Random Number Generator")

Now, our app skeleton is complete! Before moving forward, you should create an instance of the RandomNumber class and run it in your terminal or IDE to see the interface:

import kivy from kivy.app import App from kivy.uix.label import Label class RandomNumber(App):  def build(self):    return Label(text="Random Number Generator") randomApp = RandomNumber() randomApp.run()

When you run the class instance with the text Random Number Generator, you should see a simple interface or window that looks like the screenshot below:

 

Simple interface after running the code

You won鈥檛 be able to run the text on Android until you鈥檝e finished building the whole thing.

Outsourcing the interface

Next, we鈥檒l need a way to outsource the interface. First, we鈥檒l create a Kivy file in our directory that will house most of our design work. You鈥檒l want to name this file the same name as your class using lowercase letters and a .kv extension. Kivy will automatically associate the class name and the file name, but it may not work on Android if they are exactly the same.

Inside that .kv file, you need to specify the layout for your app, including elements like the label, buttons, forms, etc. To keep this demonstration simple, I鈥檒l add a label for the title Random Number, a label that will serve as a placeholder for the random number that is generated _, and a Generate button that calls the generate function.

My .kv file looks like the code below, but you can mess around with the different values to fit your requirements:

<boxLayout>:
    orientation: "vertical"
    Label:
        text: "Random Number"
        font_size: 30
        color: 0, 0.62, 0.96

    Label:
        text: "_"
        font_size: 30

    Button:
        text: "Generate"
        font_size: 15 

In the main.py file, you no longer need the Label import statement because the Kivy file takes care of your UI. However, you do need to import boxlayout, which you will use in the Kivy file.

In your main file, you need to add the import statement and edit your main.py file to read return BoxLayout() in the build method:

from kivy.uix.boxlayout import BoxLayout

If you run the command above, you should see a simple interface that has the random number title, the _ place holder, and the clickable generate button:

Random Number app rendered

Notice that you didn鈥檛 have to import anything for the Kivy file to work. Basically, when you run the app, it returns boxlayout by looking for a file inside the Kivy file with the same name as your class. Keep in mind, this is a simple interface, and you can make your app as robust as you want. Be sure to check out the Kv language documentation.

Generate the random number function

Now that our app is almost done, we鈥檒l need a simple function to generate random numbers when a user clicks the generate button, then render that random number into the app interface. To do so, we鈥檒l need to change a few things in our files.

First, we鈥檒l import the module that we鈥檒l use to generate a random number with import random. Then, we鈥檒l create a function or method that calls the generated number. For this demonstration, I鈥檒l use a range between 0 and 2000. Generating the random number is simple with the random.randint(0, 2000) command. We鈥檒l add this into our code in a moment.

Next, we鈥檒l create another class that will be our own version of the box layout. Our class will have to inherit the box layout class, which houses the method to generate random numbers and render them on the interface:

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

Within that class, we鈥檒l create the generate method, which will not only generate random numbers but also manipulate the label that controls what is displayed as the random number in the Kivy file.

To accommodate this method, we鈥檒l first need to make changes to the .kv file . Since the MyRoot class has inherited the box layout, you can make MyRoot the top level element in your .kv file:

<MyRoot>:
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

Notice that you are still keeping all your UI specifications indented in the Box Layout. After this, you need to add an ID to the label that will hold the generated numbers, making it easy to manipulate when the generate function is called. You need to specify the relationship between the ID in this file and another in the main code at the top, just before the BoxLayout line:

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

The random_label: random_label line basically means that the label with the ID random_label will be mapped to random_label in the main.py file, meaning that any action that manipulates random_label will be mapped on the label with the specified name.

We can now create the method to generate the random number in the main file:

def generate_number(self):
    self.random_label.text = str(random.randint(0, 2000))

# notice how the class method manipulates the text attributre of the random label by a# ssigning it a new random number generate by the 'random.randint(0, 2000)' funcion. S# ince this the random number generated is an integer, typecasting is required to make # it a string otherwise you will get a typeError in your terminal when you run it.

The MyRoot class should look like the code below:

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

    def generate_number(self):
        self.random_label.text = str(random.randint(0, 2000))

Congratulations! You鈥檙e now done with the main file of the app. The only thing left to do is make sure that you call this function when the generate button is clicked. You need only add the line on_press: root.generate_number() to the button selection part of your .kv file:

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15
            on_press: root.generate_number()

Now, you can run the app.

Compiling our app on Android

Before compiling our app on Android, I have some bad news for Windows users. You鈥檒l need Linux or macOS to compile your Android application. However, you don鈥檛 need to have a separate Linux distribution, instead, you can use a virtual machine.

To compile and generate a full Android .apk application, we鈥檒l use a tool called Buildozer. Let鈥檚 install Buildozer through our terminal using one of the commands below:

pip3 install buildozer
//
pip install buildozer

Now, we鈥檒l install some of Buildozer鈥檚 required dependencies. I am on Linux Ergo, so I鈥檒l use Linux-specific commands. You should execute these commands one by one:

sudo apt update
sudo apt install -y git zip unzip openjdk-13-jdk python3-pip autoconf libtool pkg-config zlib1g-dev libncurses5-dev libncursesw5-dev libtinfo5 cmake libffi-dev libssl-dev

pip3 install --upgrade Cython==0.29.19 virtualenv 

# add the following line at the end of your ~/.bashrc file
export PATH=$PATH:~/.local/bin/

After executing the specific commands, run buildozer init. You should see an output similar to the screenshot below:

Buildozer successful initialization

The command above creates a Buildozer .spec file, which you can use to make specifications to your app, including the name of the app, the icon, etc. The .spec file should look like the code block below:

[app]

# (str) Title of your application
title = My Application

# (str) Package name
package.name = myapp

# (str) Package domain (needed for android/ios packaging)
package.domain = org.test

# (str) Source code where the main.py live
source.dir = .

# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas

# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*,images/*.png

# (list) Source files to exclude (let empty to not exclude anything)
#source.exclude_exts = spec

# (list) List of directory to exclude (let empty to not exclude anything)
#source.exclude_dirs = tests, bin

# (list) List of exclusions using pattern matching
#source.exclude_patterns = license,images/*/*.jpg

# (str) Application versioning (method 1)
version = 0.1

# (str) Application versioning (method 2)
# version.regex = __version__ = \['"\](.*)['"]
# version.filename = %(source.dir)s/main.py

# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3,kivy

# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy

# (list) Garden requirements
#garden_requirements =

# (str) Presplash of the application
#presplash.filename = %(source.dir)s/data/presplash.png

# (str) Icon of the application
#icon.filename = %(source.dir)s/data/icon.png

# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait

# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY

#
# OSX Specific
#

#
# author = 漏 Copyright Info

# change the major version of python used by the app
osx.python_version = 3

# Kivy version to use
osx.kivy_version = 1.9.1

#
# Android specific
#

# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0

# (string) Presplash background color (for new android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF

# (list) Permissions
#android.permissions = INTERNET

# (int) Target Android API, should be as high as possible.
#android.api = 27

# (int) Minimum API your APK will support.
#android.minapi = 21

# (int) Android SDK version to use
#android.sdk = 20

# (str) Android NDK version to use
#android.ndk = 19b

# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
#android.ndk_api = 21

# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True

# (str) Android NDK directory (if empty, it will be automatically downloaded.)
#android.ndk_path =

# (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path =

# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =

# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
# android.skip_update = False

# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
# android.accept_sdk_license = False

# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity

# (str) Android app theme, default is ok for Kivy-based app
# android.apptheme = "@android:style/Theme.NoTitleBar"

# (list) Pattern to whitelist for the whole project
#android.whitelist =

# (str) Path to a custom whitelist file
#android.whitelist_src =

# (str) Path to a custom blacklist file
#android.blacklist_src =

# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar

# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
#android.add_src =

# (list) Android AAR archives to add (currently works only with sdl2_gradle
# bootstrap)
#android.add_aars =

# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
#android.gradle_dependencies =

# (list) add java compile options
# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
# see https://developer.android.com/studio/write/java8-support for further information
# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"

# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
# please enclose in double quotes 
# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
#android.add_gradle_repositories =

# (list) packaging options to add 
# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
# can be necessary to solve conflicts in gradle_dependencies
# please enclose in double quotes 
# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
#android.add_gradle_repositories =

# (list) Java classes to add as activities to the manifest.
#android.add_activities = com.example.ExampleActivity

# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME

# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png

# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =

# (str) launchMode to set for the main activity
#android.manifest.launch_mode = standard

# (list) Android additional libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android/*.so
#android.add_libs_armeabi_v7a = libs/android-v7/*.so
#android.add_libs_arm64_v8a = libs/android-v8/*.so
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so

# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False

# (list) Android application meta-data to set (key=value format)
#android.meta_data =

# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =

# (list) Android shared libraries which will be added to AndroidManifest.xml using <uses-library> tag
#android.uses_library =

# (str) Android logcat filters to use
#android.logcat_filters = *:S python:D

# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1

# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = armeabi-v7a

# (int) overrides automatic versionCode computation (used in build.gradle)
# this is not the same as app version and should only be edited if you know what you're doing
# android.numeric_version = 1

#
# Python for android (p4a) specific
#

# (str) python-for-android fork to use, defaults to upstream (kivy)
#p4a.fork = kivy

# (str) python-for-android branch to use, defaults to master
#p4a.branch = master

# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
#p4a.source_dir =

# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =

# (str) Filename to the hook for p4a
#p4a.hook =

# (str) Bootstrap to use for android builds
# p4a.bootstrap = sdl2

# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =


#
# iOS specific
#

# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# Alternately, specify the URL and branch of a git checkout:
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master

# Another platform dependency: ios-deploy
# Uncomment to use a custom checkout
#ios.ios_deploy_dir = ../ios_deploy
# Or specify URL and branch
ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
ios.ios_deploy_branch = 1.7.0

# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"

# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s


[buildozer]

# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2

# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1

# (str) Path to build artifact storage, absolute or relative to spec file
# build_dir = ./.buildozer

# (str) Path to build output (i.e. .apk, .ipa) storage
# bin_dir = ./bin

#    -----------------------------------------------------------------------------
#    List as sections
#
#    You can define all the "list" as [section:key].
#    Each line will be considered as a option to the list.
#    Let's take [app] / source.exclude_patterns.
#    Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
#    This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#


#    -----------------------------------------------------------------------------
#    Profiles
#
#    You can extend section / key with a profile
#    For example, you want to deploy a demo version of your application without
#    HD content. You could first change the title to add "(demo)" in the name
#    and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
#    Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug

If you want to specify things like the icon, requirements, loading screen, etc., you should edit this file. After making all the desired edits to your application, run buildozer -v android debug from your app directory to build and compile your application. This may take a while, especially if you have a slow machine.

After the process is done, your terminal should have some logs, one confirming that the build was successful:

Android successful build

You should also have an APK version of your app in your bin directory. This is the application executable that you will install and run on your phone:

Android .apk in the bin directory

Conclusion

Congratulations! If you have followed this tutorial step by step, you should have a simple random number generator app on your phone. Play around with it and tweak some values, then rebuild. Running the rebuild will not take as much time as the first build.

As you can see, building a mobile application with Python is fairly straightforward, as long as you are familiar with the framework or module you are working with. Regardless, the logic is executed the same way.

Get familiar with the Kivy module and it鈥檚 widgets. You can never know everything all at once. You only need to find a project and get your feet wet as early as possible. Happy coding.

Link: https://blog.logrocket.com/build-android-application-kivy-python-framework/

#python 

Cree Una Aplicaci贸n De Android Con El Marco Kivy Python

Si es un desarrollador de Python que est谩 pensando en comenzar con el desarrollo m贸vil, entonces el marco Kivy es su mejor opci贸n. Con Kivy, puede desarrollar aplicaciones independientes de la plataforma que compilan para iOS, Android, Windows, macOS y Linux. En este art铆culo, cubriremos Android espec铆ficamente porque es el m谩s utilizado.

Construiremos una aplicaci贸n generadora de n煤meros aleatorios simple que puede instalar en su tel茅fono y probar cuando haya terminado. Para continuar con este art铆culo, debe estar familiarizado con Python. 隆Empecemos!

Primeros pasos con Kivy

Primero, necesitar谩 un nuevo directorio para su aplicaci贸n. Aseg煤rese de tener Python instalado en su m谩quina y abra un nuevo archivo de Python. Deber谩 instalar el m贸dulo Kivy desde su terminal usando cualquiera de los comandos a continuaci贸n. Para evitar conflictos de paquetes, aseg煤rese de instalar Kivy en un entorno virtual:

pip install kivy 
//
pip3 install kivy 

Una vez que haya instalado Kivy, deber铆a ver un mensaje de 茅xito de su terminal que se parece a las capturas de pantalla a continuaci贸n:

Instalaci贸n decepcionada

Instalaci贸n exitosa de Kivy

 

A continuaci贸n, navegue a la carpeta de su proyecto. En el main.pyarchivo, necesitaremos importar el m贸dulo Kivy y especificar qu茅 versi贸n queremos. Puede usar Kivy v2.0.0, pero si tiene un tel茅fono inteligente anterior a Android 8.0, le recomiendo usar Kivy v1.9.0. Puede jugar con las diferentes versiones durante la compilaci贸n para ver las diferencias en las caracter铆sticas y el rendimiento.

Agregue el n煤mero de versi贸n justo despu茅s de la import kivyl铆nea de la siguiente manera:

kivy.require('1.9.0')

Ahora, crearemos una clase que b谩sicamente definir谩 nuestra aplicaci贸n; Voy a nombrar el m铆o RandomNumber. Esta clase heredar谩 la appclase de Kivy. Por lo tanto, debe importar appagregando from kivy.app import App:

class RandomNumber(App): 

En la RandomNumberclase, deber谩 agregar una funci贸n llamada build, que toma un selfpar谩metro. Para devolver la interfaz de usuario, usaremos la buildfunci贸n. Por ahora, lo tengo devuelto como una simple etiqueta. Para hacerlo, deber谩 importar Labelusando la l铆nea from kivy.uix.label import Label:

import kivy
from kivy.app import App
from kivy.uix.label import Label

class RandomNumber(App):
  def build(self):
    return Label(text="Random Number Generator")

隆Ahora, el esqueleto de nuestra aplicaci贸n est谩 completo! Antes de continuar, debe crear una instancia de la RandomNumberclase y ejecutarla en su terminal o IDE para ver la interfaz:

importar kivy de kivy.app importar aplicaci贸n de kivy.uix.label clase de etiqueta de importaci贸n RandomNumber(App): def build(self): return Label(text="Generador de n煤meros aleatorios") randomApp = RandomNumber() randomApp.run()

Cuando ejecuta la instancia de clase con el texto Random Number Generator, deber铆a ver una interfaz o ventana simple que se parece a la siguiente captura de pantalla:

 

Interfaz simple despu茅s de ejecutar el c贸digo.

No podr谩 ejecutar el texto en Android hasta que haya terminado de construir todo.

Externalizaci贸n de la interfaz

A continuaci贸n, necesitaremos una forma de subcontratar la interfaz. Primero, crearemos un archivo Kivy en nuestro directorio que albergar谩 la mayor parte de nuestro trabajo de dise帽o. Querr谩 nombrar este archivo con el mismo nombre que su clase usando letras min煤sculas y una .kvextensi贸n. Kivy asociar谩 autom谩ticamente el nombre de la clase y el nombre del archivo, pero es posible que no funcione en Android si son exactamente iguales.

Dentro de ese .kvarchivo, debe especificar el dise帽o de su aplicaci贸n, incluidos elementos como la etiqueta, los botones, los formularios, etc. Para simplificar esta demostraci贸n, agregar茅 una etiqueta para el t铆tulo Random Number, una etiqueta que servir谩 como marcador de posici贸n. para el n煤mero aleatorio que se genera _, y un Generatebot贸n que llama a la generatefunci贸n.

Mi .kvarchivo se parece al siguiente c贸digo, pero puede jugar con los diferentes valores para que se ajusten a sus requisitos:

<boxLayout>:
    orientation: "vertical"
    Label:
        text: "Random Number"
        font_size: 30
        color: 0, 0.62, 0.96

    Label:
        text: "_"
        font_size: 30

    Button:
        text: "Generate"
        font_size: 15 

En el main.pyarchivo, ya no necesita la Labeldeclaraci贸n de importaci贸n porque el archivo Kivy se encarga de su interfaz de usuario. Sin embargo, necesita importar boxlayout, que utilizar谩 en el archivo Kivy.

En su archivo principal, debe agregar la declaraci贸n de importaci贸n y editar su main.pyarchivo para leer return BoxLayout()el buildm茅todo:

from kivy.uix.boxlayout import BoxLayout

Si ejecuta el comando anterior, deber铆a ver una interfaz simple que tiene el t铆tulo del n煤mero aleatorio, el _marcador de posici贸n y el generatebot贸n en el que se puede hacer clic:

Aplicaci贸n de n煤meros aleatorios renderizada

Tenga en cuenta que no tuvo que importar nada para que funcione el archivo Kivy. B谩sicamente, cuando ejecuta la aplicaci贸n, regresa boxlayoutbuscando un archivo dentro del archivo Kivy con el mismo nombre que su clase. Tenga en cuenta que esta es una interfaz simple y puede hacer que su aplicaci贸n sea tan robusta como desee. Aseg煤rese de consultar la documentaci贸n del idioma Kv .

Generar la funci贸n de n煤meros aleatorios

Ahora que nuestra aplicaci贸n est谩 casi terminada, necesitaremos una funci贸n simple para generar n煤meros aleatorios cuando un usuario haga clic en el generatebot贸n y luego mostrar ese n煤mero aleatorio en la interfaz de la aplicaci贸n. Para hacerlo, necesitaremos cambiar algunas cosas en nuestros archivos.

Primero, importaremos el m贸dulo que usaremos para generar un n煤mero aleatorio con import random. Luego, crearemos una funci贸n o m茅todo que llame al n煤mero generado. Para esta demostraci贸n, usar茅 un rango entre 0y 2000. Generar el n煤mero aleatorio es simple con el random.randint(0, 2000)comando. Agregaremos esto a nuestro c贸digo en un momento.

A continuaci贸n, crearemos otra clase que ser谩 nuestra propia versi贸n del box layout. Nuestra clase tendr谩 que heredar la box layoutclase, que alberga el m茅todo para generar n煤meros aleatorios y representarlos en la interfaz:

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

Dentro de esa clase, crearemos el generatem茅todo, que no solo generar谩 n煤meros aleatorios, sino que tambi茅n manipular谩 la etiqueta que controla lo que se muestra como n煤mero aleatorio en el archivo Kivy.

Para acomodar este m茅todo, primero necesitaremos hacer cambios en el .kvarchivo. Dado que la MyRootclase ha heredado el box layout, puede crear MyRootel elemento de nivel superior en su .kvarchivo:

<MyRoot>:
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

Tenga en cuenta que todav铆a mantiene todas las especificaciones de la interfaz de usuario con sangr铆a en el archivo Box Layout. Despu茅s de esto, debe agregar una identificaci贸n a la etiqueta que contendr谩 los n煤meros generados, lo que facilita la manipulaci贸n cuando generatese llama a la funci贸n. Debe especificar la relaci贸n entre la ID en este archivo y otra en el c贸digo principal en la parte superior, justo antes de la BoxLayoutl铆nea:

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

La random_label: random_labell铆nea b谩sicamente significa que la etiqueta con el ID random_labelse asignar谩 a random_labelen el main.pyarchivo, lo que significa que cualquier acci贸n que manipula random_labelser谩n mapeados en la etiqueta con el nombre especificado.

Ahora podemos crear el m茅todo para generar el n煤mero aleatorio en el archivo principal:

def generate_number(self):
    self.random_label.text = str(random.randint(0, 2000))

# notice how the class method manipulates the text attributre of the random label by a# ssigning it a new random number generate by the 'random.randint(0, 2000)' funcion. S# ince this the random number generated is an integer, typecasting is required to make # it a string otherwise you will get a typeError in your terminal when you run it.

La MyRootclase deber铆a parecerse al siguiente c贸digo:

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

    def generate_number(self):
        self.random_label.text = str(random.randint(0, 2000))

隆Felicidades! Ya ha terminado con el archivo principal de la aplicaci贸n. Lo 煤nico que queda por hacer es asegurarse de llamar a esta funci贸n cuando se haga generateclic en el bot贸n. Solo necesita agregar la l铆nea on_press: root.generate_number()a la parte de selecci贸n de botones de su .kvarchivo:

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15
            on_press: root.generate_number()

Ahora, puede ejecutar la aplicaci贸n.

Compilando nuestra aplicaci贸n en Android

Antes de compilar nuestra aplicaci贸n en Android, tengo malas noticias para los usuarios de Windows. Necesitar谩 Linux o macOS para compilar su aplicaci贸n de Android. Sin embargo, no necesita tener una distribuci贸n de Linux separada, en su lugar, puede usar una m谩quina virtual.

Para compilar y generar una .apkaplicaci贸n Android completa , usaremos una herramienta llamada Buildozer . Instalemos Buildozer a trav茅s de nuestra terminal usando uno de los siguientes comandos:

pip3 install buildozer
//
pip install buildozer

Ahora, instalaremos algunas de las dependencias requeridas de Buildozer. Estoy en Linux Ergo, as铆 que usar茅 comandos espec铆ficos de Linux. Debe ejecutar estos comandos uno por uno:

sudo apt update
sudo apt install -y git zip unzip openjdk-13-jdk python3-pip autoconf libtool pkg-config zlib1g-dev libncurses5-dev libncursesw5-dev libtinfo5 cmake libffi-dev libssl-dev

pip3 install --upgrade Cython==0.29.19 virtualenv 

# add the following line at the end of your ~/.bashrc file
export PATH=$PATH:~/.local/bin/

Despu茅s de ejecutar los comandos espec铆ficos, ejecute buildozer init. Deber铆a ver un resultado similar a la captura de pantalla a continuaci贸n:

Inicializaci贸n exitosa de Buildozer

El comando anterior crea un .specarchivo Buildozer , que puede usar para hacer especificaciones para su aplicaci贸n, incluido el nombre de la aplicaci贸n, el 铆cono, etc. El .specarchivo debe verse como el bloque de c贸digo a continuaci贸n:

[app]

# (str) Title of your application
title = My Application

# (str) Package name
package.name = myapp

# (str) Package domain (needed for android/ios packaging)
package.domain = org.test

# (str) Source code where the main.py live
source.dir = .

# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas

# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*,images/*.png

# (list) Source files to exclude (let empty to not exclude anything)
#source.exclude_exts = spec

# (list) List of directory to exclude (let empty to not exclude anything)
#source.exclude_dirs = tests, bin

# (list) List of exclusions using pattern matching
#source.exclude_patterns = license,images/*/*.jpg

# (str) Application versioning (method 1)
version = 0.1

# (str) Application versioning (method 2)
# version.regex = __version__ = \['"\](.*)['"]
# version.filename = %(source.dir)s/main.py

# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3,kivy

# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy

# (list) Garden requirements
#garden_requirements =

# (str) Presplash of the application
#presplash.filename = %(source.dir)s/data/presplash.png

# (str) Icon of the application
#icon.filename = %(source.dir)s/data/icon.png

# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait

# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY

#
# OSX Specific
#

#
# author = 漏 Copyright Info

# change the major version of python used by the app
osx.python_version = 3

# Kivy version to use
osx.kivy_version = 1.9.1

#
# Android specific
#

# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0

# (string) Presplash background color (for new android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF

# (list) Permissions
#android.permissions = INTERNET

# (int) Target Android API, should be as high as possible.
#android.api = 27

# (int) Minimum API your APK will support.
#android.minapi = 21

# (int) Android SDK version to use
#android.sdk = 20

# (str) Android NDK version to use
#android.ndk = 19b

# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
#android.ndk_api = 21

# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True

# (str) Android NDK directory (if empty, it will be automatically downloaded.)
#android.ndk_path =

# (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path =

# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =

# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
# android.skip_update = False

# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
# android.accept_sdk_license = False

# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity

# (str) Android app theme, default is ok for Kivy-based app
# android.apptheme = "@android:style/Theme.NoTitleBar"

# (list) Pattern to whitelist for the whole project
#android.whitelist =

# (str) Path to a custom whitelist file
#android.whitelist_src =

# (str) Path to a custom blacklist file
#android.blacklist_src =

# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar

# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
#android.add_src =

# (list) Android AAR archives to add (currently works only with sdl2_gradle
# bootstrap)
#android.add_aars =

# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
#android.gradle_dependencies =

# (list) add java compile options
# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
# see https://developer.android.com/studio/write/java8-support for further information
# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"

# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
# please enclose in double quotes 
# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
#android.add_gradle_repositories =

# (list) packaging options to add 
# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
# can be necessary to solve conflicts in gradle_dependencies
# please enclose in double quotes 
# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
#android.add_gradle_repositories =

# (list) Java classes to add as activities to the manifest.
#android.add_activities = com.example.ExampleActivity

# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME

# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png

# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =

# (str) launchMode to set for the main activity
#android.manifest.launch_mode = standard

# (list) Android additional libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android/*.so
#android.add_libs_armeabi_v7a = libs/android-v7/*.so
#android.add_libs_arm64_v8a = libs/android-v8/*.so
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so

# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False

# (list) Android application meta-data to set (key=value format)
#android.meta_data =

# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =

# (list) Android shared libraries which will be added to AndroidManifest.xml using <uses-library> tag
#android.uses_library =

# (str) Android logcat filters to use
#android.logcat_filters = *:S python:D

# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1

# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = armeabi-v7a

# (int) overrides automatic versionCode computation (used in build.gradle)
# this is not the same as app version and should only be edited if you know what you're doing
# android.numeric_version = 1

#
# Python for android (p4a) specific
#

# (str) python-for-android fork to use, defaults to upstream (kivy)
#p4a.fork = kivy

# (str) python-for-android branch to use, defaults to master
#p4a.branch = master

# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
#p4a.source_dir =

# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =

# (str) Filename to the hook for p4a
#p4a.hook =

# (str) Bootstrap to use for android builds
# p4a.bootstrap = sdl2

# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =


#
# iOS specific
#

# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# Alternately, specify the URL and branch of a git checkout:
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master

# Another platform dependency: ios-deploy
# Uncomment to use a custom checkout
#ios.ios_deploy_dir = ../ios_deploy
# Or specify URL and branch
ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
ios.ios_deploy_branch = 1.7.0

# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"

# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s


[buildozer]

# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2

# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1

# (str) Path to build artifact storage, absolute or relative to spec file
# build_dir = ./.buildozer

# (str) Path to build output (i.e. .apk, .ipa) storage
# bin_dir = ./bin

#    -----------------------------------------------------------------------------
#    List as sections
#
#    You can define all the "list" as [section:key].
#    Each line will be considered as a option to the list.
#    Let's take [app] / source.exclude_patterns.
#    Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
#    This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#


#    -----------------------------------------------------------------------------
#    Profiles
#
#    You can extend section / key with a profile
#    For example, you want to deploy a demo version of your application without
#    HD content. You could first change the title to add "(demo)" in the name
#    and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
#    Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug

Si desea especificar cosas como el 铆cono, los requisitos, la pantalla de carga, etc., debe editar este archivo. Despu茅s de realizar todas las ediciones deseadas en su aplicaci贸n, ejecute buildozer -v android debugdesde el directorio de su aplicaci贸n para construir y compilar su aplicaci贸n. Esto puede llevar un tiempo, especialmente si tiene una m谩quina lenta.

Una vez finalizado el proceso, su terminal deber铆a tener algunos registros, uno que confirme que la compilaci贸n fue exitosa:

Construcci贸n exitosa de Android

Tambi茅n debe tener una versi贸n APK de su aplicaci贸n en su directorio bin. Este es el ejecutable de la aplicaci贸n que instalar谩 y ejecutar谩 en su tel茅fono:

Android .apk en el directorio bin

Conclusi贸n

隆Felicidades! Si ha seguido este tutorial paso a paso, deber铆a tener una aplicaci贸n simple de generador de n煤meros aleatorios en su tel茅fono. Juega con 茅l y ajusta algunos valores, luego reconstruye. Ejecutar la reconstrucci贸n no llevar谩 tanto tiempo como la primera compilaci贸n.

Como puede ver, crear una aplicaci贸n m贸vil con Python es bastante sencillo , siempre que est茅 familiarizado con el marco o m贸dulo con el que est谩 trabajando. Independientemente, la l贸gica se ejecuta de la misma manera.

Familiar铆cese con el m贸dulo Kivy y sus widgets. Nunca se puede saber todo a la vez. Solo necesita encontrar un proyecto y mojarse los pies lo antes posible. Codificaci贸n feliz.

Enlace: https://blog.logrocket.com/build-android-application-kivy-python-framework/

#python 

KivyPython銉曘儸銉笺儬銉兗銈倰浣跨敤銇椼仸Android銈€儣銉偙銉笺偡銉с兂銈掓绡夈仚銈

銇傘仾銇熴亴銉€儛銈ゃ儷闁嬬櫤銈掑銈併倠銇撱仺銈掕冦亪銇︺亜銈婸ython闁嬬櫤鑰呫仾銈銆並ivy銉曘儸銉笺儬銉兗銈銇屾渶鍠勩伄绛栥仹銇欍侹ivy銈掍娇鐢ㄣ仚銈嬨仺銆乮OS銆丄ndroid銆乄indows銆乵acOS銆併亰銈堛伋Linux鐢ㄣ伀銈炽兂銉戙偆銉仌銈屻倠銉椼儵銉冦儓銉曘偐銉笺儬銇緷瀛樸仐銇亜銈€儣銉偙銉笺偡銉с兂銈掗枊鐧恒仹銇嶃伨銇欍傘亾銇浜嬨仹銇丄ndroid銇屾渶銈備娇鐢ㄣ仌銈屻仸銇勩倠銇熴倎銆佺壒銇獳ndroid銇仱銇勩仸瑾槑銇椼伨銇欍

绨″崢銇贡鏁般偢銈с儘銉兗銈裤兗銈€儣銉倰浣滄垚銇椼伨銇欍傘亾銇偄銉椼儶銈掓惡甯浕瑭便伀銈ゃ兂銈广儓銉笺儷銇椼仸銆佸畬浜嗐仐銇熴倝銉嗐偣銉堛仹銇嶃伨銇欍傘亾銇浜嬨倰缍氥亼銈嬨伀銇丳ython銇簿閫氥仐銇︺亜銈嬪繀瑕併亴銇傘倞銇俱仚銆傚銈併伨銇椼倗銇嗭紒

Kivy銈掍娇銇勫銈併倠

銇俱仛銆併偄銉椼儶鐢ㄣ伄鏂般仐銇勩儑銈c儸銈儓銉亴蹇呰銇仾銈娿伨銇欍傘優銈枫兂銇玃ython銇屻偆銉炽偣銉堛兗銉仌銈屻仸銇勩倠銇撱仺銈掔⒑瑾嶃仐銆佹柊銇椼亜Python銉曘偂銈ゃ儷銈掗枊銇嶃伨銇欍備互涓嬨伄銈炽優銉炽儔銇亜銇氥倢銇嬨倰浣跨敤銇椼仸銆併偪銉笺儫銉娿儷銇嬨倝Kivy銉€偢銉ャ兗銉倰銈ゃ兂銈广儓銉笺儷銇欍倠蹇呰銇屻亗銈娿伨銇欍傘儜銉冦偙銉笺偢銇鍚堛倰閬裤亼銈嬨仧銈併伀銆並ivy銈掍划鎯崇挵澧冦伀銈ゃ兂銈广儓銉笺儷銇椼仸銇勩倠銇撱仺銈掔⒑瑾嶃仐銇︺亸銇犮仌銇勩

pip install kivy 
//
pip3 install kivy 

Kivy銈掋偆銉炽偣銉堛兗銉仚銈嬨仺銆佷互涓嬨伄銈广偗銉兗銉炽偡銉с儍銉堛伄銈堛亞銇垚鍔熴儭銉冦偦銉笺偢銇屻偪銉笺儫銉娿儷銇嬨倝琛ㄧず銇曘倢銇俱仚銆

銇屻仯銇嬨倞銇椼仧銈ゃ兂銈广儓銉笺儷

Kivy銇偆銉炽偣銉堛兗銉伀鎴愬姛

 

娆°伀銆併儣銉偢銈с偗銉堛儠銈┿儷銉銇Щ鍕曘仐銇俱仚銆傘亾銇main.py銉曘偂銈ゃ儷銇с並ivy銉€偢銉ャ兗銉倰銈ゃ兂銉濄兗銉堛仐銆佸繀瑕併仾銉愩兗銈搞儳銉炽倰鎸囧畾銇欍倠蹇呰銇屻亗銈娿伨銇欍侹ivy v2.0.0銈掍娇鐢ㄣ仹銇嶃伨銇欍亴銆丄ndroid 8.0銈堛倞鍙ゃ亜銈广優銉笺儓銉曘偐銉炽倰浣跨敤銇椼仸銇勩倠鍫村悎銇並ivyv1.9.0銈掍娇鐢ㄣ仚銈嬨亾銇ㄣ倰銇婂嫥銈併仐銇俱仚銆傘儞銉儔涓伀銇曘伨銇栥伨銇儛銉笺偢銉с兂銈掋亜銇樸仯銇︺佹鑳姐仺銉戙儠銈┿兗銉炪兂銈广伄閬曘亜銈掔⒑瑾嶃仹銇嶃伨銇欍

import kivy娆°伄銈堛亞銇佽銇洿寰屻伀銉愩兗銈搞儳銉崇暘鍙枫倰杩藉姞銇椼伨銇欍

kivy.require('1.9.0')

娆°伀銆佸熀鏈殑銇偄銉椼儶銈掑畾缇┿仚銈嬨偗銉┿偣銈掍綔鎴愩仐銇俱仚銆傜銇悕鍓嶃倰浠樸亼銇俱仚RandomNumber銆傘亾銇偗銉┿偣銇appKivy銇嬨倝銈儵銈广倰缍欐壙銇椼伨銇欍傘仐銇熴亴銇c仸銆佹app銈掕拷鍔犮仐銇︺偆銉炽儩銉笺儓銇欍倠蹇呰銇屻亗銈娿伨銇from kivy.app import App

class RandomNumber(App): 

銇с伅RandomNumber銈儵銈广伅銆佸懠銇冲嚭銇曘倢銇熼枹鏁般倰杩藉姞銇欍倠蹇呰銇屻亗銈娿伨銇build銇ㄣ倞銆self銉戙儵銉°兗銈裤倰銆傚疅闅涖伀UI銈掕繑銇欍伀銇併亾銇build闁㈡暟銈掍娇鐢ㄣ仐銇俱仚銆備粖銇仺銇撱倣銆佸崢绱斻仾銉┿儥銉仺銇椼仸杩旈併仐銇︺亜銇俱仚銆傘仢銇仧銈併伀銇佹Label銇銈掍娇鐢ㄣ仐銇︺偆銉炽儩銉笺儓銇欍倠蹇呰銇屻亗銈娿伨銇from kivy.uix.label import Label

import kivy
from kivy.app import App
from kivy.uix.label import Label

class RandomNumber(App):
  def build(self):
    return Label(text="Random Number Generator")

銇撱倢銇с併偄銉椼儶銇偣銈便儷銉堛兂銇屽畬鎴愩仐銇俱仐銇熴傚厛銇层個鍓嶃伀銆RandomNumber銈儵銈广伄銈ゃ兂銈广偪銉炽偣銈掍綔鎴愩仐銆併偪銉笺儫銉娿儷銇俱仧銇疘DE銇у疅琛屻仐銇︺併偆銉炽偪銉笺儠銈с偆銈广倰纰鸿獚銇欍倠蹇呰銇屻亗銈娿伨銇欍

import kivy from kivy.app import App from kivy.uix.label import Label class RandomNumber锛圓pp锛夛細def build锛坰elf锛夛細return Label锛坱ext = "Random Number Generator"锛塺andomApp = RandomNumber锛堬級randomApp.run锛堬級

銉嗐偔銈广儓銈掍娇鐢ㄣ仐銇︺偗銉┿偣銈ゃ兂銈广偪銉炽偣銈掑疅琛屻仚銈嬨仺銆Random Number Generator娆°伄銈广偗銉兗銉炽偡銉с儍銉堛伄銈堛亞銇崢绱斻仾銈ゃ兂銈裤兗銉曘偋銈ゃ偣銇俱仧銇偊銈c兂銉夈偊銇岃〃绀恒仌銈屻伨銇欍

 

銈炽兗銉夈倰瀹熻銇椼仧寰屻伄銈枫兂銉椼儷銇偆銉炽偪銉笺儠銈с偆銈

銇欍伖銇︺伄妲嬬瘔銇屽畬浜嗐仚銈嬨伨銇с丄ndroid銇с儐銈偣銉堛倰瀹熻銇欍倠銇撱仺銇仹銇嶃伨銇涖倱銆

銈ゃ兂銈裤兗銉曘偋銉笺偣銇偄銈︺儓銈姐兗銈枫兂銈

娆°伀銆併偆銉炽偪銉笺儠銈с兗銈广倰銈€偊銉堛偨銉笺偡銉炽偘銇欍倠鏂规硶銇屽繀瑕併伀銇倞銇俱仚銆傘伨銇氥併儑銈c儸銈儓銉伀Kivy銉曘偂銈ゃ儷銈掍綔鎴愩仐銇俱仚銆傘亾銇儠銈°偆銉伀銇併伝銇ㄣ倱銇┿伄瑷▓浣滄キ銇屽惈銇俱倢銇︺亜銇俱仚銆傘亾銇儠銈°偆銉伀銇佸皬鏂囧瓧銇.kv鎷″嫉瀛愩倰浣跨敤銇椼仸銆併偗銉┿偣銇ㄥ悓銇樺悕鍓嶃倰浠樸亼銈嬨亾銇ㄣ亴銇с亶銇俱仚銆侹ivy銇偗銉┿偣鍚嶃仺銉曘偂銈ゃ儷鍚嶃倰鑷嫊鐨勩伀闁㈤d粯銇戙伨銇欍亴銆併仢銈屻倝銇屻伨銇c仧銇忓悓銇樸仹銇傘倠鍫村悎銆丄ndroid銇с伅姗熻兘銇椼仾銇勫彲鑳芥с亴銇傘倞銇俱仚銆

銇濄伄.kv銉曘偂銈ゃ儷鍐呫仹銆併儵銉欍儷銆併儨銈裤兂銆併儠銈┿兗銉犮仾銇┿伄瑕佺礌銈掑惈銈銈€儣銉伄銉偆銈€偊銉堛倰鎸囧畾銇欍倠蹇呰銇屻亗銈娿伨銇欍傘亾銇儑銉€倰绨″崢銇仚銈嬨仧銈併伀銆併偪銈ゃ儓銉Random Number銇儵銉欍儷銆併儣銉兗銈广儧銉儉銉笺仺銇椼仸姗熻兘銇欍倠銉┿儥銉倰杩藉姞銇椼伨銇欍傜敓鎴愩仌銈屻倠涔辨暟_銆併亰銈堛伋闁㈡暟Generate銈掑懠銇冲嚭銇欍儨銈裤兂generate

绉併伄.kv銉曘偂銈ゃ儷銇互涓嬨伄銈炽兗銉夈伄銈堛亞銇銇堛伨銇欍亴銆佽浠躲伀鍚堛倧銇涖仸銇曘伨銇栥伨銇ゃ倰銇勩仒銈嬨亾銇ㄣ亴銇с亶銇俱仚銆

<boxLayout>:
    orientation: "vertical"
    Label:
        text: "Random Number"
        font_size: 30
        color: 0, 0.62, 0.96

    Label:
        text: "_"
        font_size: 30

    Button:
        text: "Generate"
        font_size: 15 

銇撱伄main.py銉曘偂銈ゃ儷銇с伅Label銆並ivy銉曘偂銈ゃ儷銇孶I銈掑嚘鐞嗐仚銈嬨仧銈併乮mport銈广儐銉笺儓銉°兂銉堛伅涓嶈銇仾銈娿伨銇椼仧銆傘仧銇犮仐銆boxlayoutKivy銉曘偂銈ゃ儷銇т娇鐢ㄣ仚銈嬨倰銈ゃ兂銉濄兗銉堛仚銈嬪繀瑕併亴銇傘倞銇俱仚銆

銉°偆銉炽儠銈°偆銉仹銆乮mport銈广儐銉笺儓銉°兂銉堛倰杩藉姞銇椼main.py銉曘偂銈ゃ儷銈掔法闆return BoxLayout()銇椼仸build銉°偨銉冦儔銇ц銇垮彇銈嬪繀瑕併亴銇傘倞銇俱仚銆

from kivy.uix.boxlayout import BoxLayout

涓婅銇偝銉炪兂銉夈倰瀹熻銇欍倠銇ㄣ佷贡鏁般伄銈裤偆銉堛儷銆_銉椼儸銉笺偣銉涖儷銉銉笺併亰銈堛伋銈儶銉冦偗鍙兘銇generate銉溿偪銉炽倰鍌欍亪銇熴偡銉炽儣銉仾銈ゃ兂銈裤兗銉曘偋銈ゃ偣銇岃〃绀恒仌銈屻伨銇欍

銉兂銉銉兂銈般仌銈屻仧涔辨暟銈€儣銉

Kivy銉曘偂銈ゃ儷銈掓鑳姐仌銇涖倠銇熴倎銇綍銈傘偆銉炽儩銉笺儓銇欍倠蹇呰銇屻仾銇嬨仯銇熴亾銇ㄣ伀娉ㄦ剰銇椼仸銇忋仩銇曘亜銆傚熀鏈殑銇併偄銉椼儶銈掑疅琛屻仚銈boxlayout銇ㄣ併偗銉┿偣銇ㄥ悓銇樺悕鍓嶃伄Kivy銉曘偂銈ゃ儷鍐呫伄銉曘偂銈ゃ儷銈掓绱€仐銇︽埢銈娿伨銇欍傘亾銈屻伅銈枫兂銉椼儷銇偆銉炽偪銉笺儠銈с兗銈广仹銇傘倞銆併偄銉椼儶銈掑繀瑕併伀蹇溿仒銇﹀爡鐗€伀銇欍倠銇撱仺銇屻仹銇嶃伨銇欍Kv瑷瑾炪伄銉夈偔銉ャ儭銉炽儓銈蹇呫仛纰鸿獚銇椼仸銇忋仩銇曘亜銆

涔辨暟闁㈡暟銈掔敓鎴愩仚銈

銈€儣銉亴銇汇伡瀹屾垚銇椼仧銇仹銆併儲銉笺偠銉笺亴generate銉溿偪銉炽倰銈儶銉冦偗銇椼仧銇ㄣ亶銇贡鏁般倰鐢熸垚銇椼併仢銇贡鏁般倰銈€儣銉伄銈ゃ兂銈裤兗銉曘偋銈ゃ偣銇儸銉炽儉銉兂銈般仚銈嬬啊鍗樸仾闁㈡暟銇屽繀瑕併伀銇倞銇俱仚銆傘仢銇仧銈併伀銇併儠銈°偆銉唴銇亜銇忋仱銇嬨伄澶夋洿銈掕銇嗗繀瑕併亴銇傘倞銇俱仚銆

銇俱仛銆併仹涔辨暟銈掔敓鎴愩仚銈嬨仧銈併伀浣跨敤銇欍倠銉€偢銉ャ兗銉倰銈ゃ兂銉濄兗銉堛仐銇俱仚import random銆傛銇佺敓鎴愩仌銈屻仧鐣彿銈掑懠銇冲嚭銇欓枹鏁般伨銇熴伅銉°偨銉冦儔銈掍綔鎴愩仐銇俱仚銆傘亾銇儑銉€仹銇佺銇枔銇瘎鍥层倰浣跨敤銇椼伨銇02000銆傘亾銇random.randint(0, 2000)銈炽優銉炽儔銈掍娇鐢ㄣ仚銈嬨仺銆佷贡鏁般倰绨″崢銇敓鎴愩仹銇嶃伨銇欍傘亾銈屻倰銇欍亹銇偝銉笺儔銇拷鍔犮仐銇俱仚銆

娆°伀銆佺嫭鑷伄銉愩兗銈搞儳銉炽仺銇倠鍒ャ伄銈儵銈广倰浣滄垚銇椼伨銇box layout銆傘亾銇box layout銈儵銈广伅銆佷贡鏁般倰鐢熸垚銇椼仸銈ゃ兂銈裤兗銉曘偋銈ゃ偣涓娿仹銉兂銉銉兂銈般仚銈嬨儭銈姐儍銉夈倰鍚個銈儵銈广倰缍欐壙銇欍倠蹇呰銇屻亗銈娿伨銇欍

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

銇濄伄銈儵銈瑰唴銇сgenerate涔辨暟銈掔敓鎴愩仚銈嬨仩銇戙仹銇亸銆並ivy銉曘偂銈ゃ儷銇贡鏁般仺銇椼仸琛ㄧず銇曘倢銈嬨倐銇倰鍒跺尽銇欍倠銉┿儥銉倰鎿嶄綔銇欍倠銉°偨銉冦儔銈掍綔鎴愩仐銇俱仚銆

銇撱伄鏂规硶銇蹇溿仚銈嬨伀銇佹渶鍒濄伀.kv銉曘偂銈ゃ儷銇鏇淬倰鍔犮亪銈嬪繀瑕併亴銇傘倞銇俱仚銆備互鏉MyRoot銈儵銈广亴缍欐壙銇椼仸銇勩倠box layout銆併亗銇仧銇屼綔銈嬨亾銇ㄣ亴銇с亶銈MyRoot銇傘仾銇熴伄銉堛儍銉椼儸銉欍儷銇绱.kv銉曘偂銈ゃ儷銈掞細

<MyRoot>:
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

銇с偆銉炽儑銉炽儓銇曘倢銇熴仚銇广仸銇甎I浠曟銈掍繚鎸併仐銇︺亜銈嬨亾銇ㄣ伀娉ㄦ剰銇椼仸銇忋仩銇曘亜Box Layout銆傘亾銇緦銆佺敓鎴愩仌銈屻仧鐣彿銈掍繚鎸併仚銈婭D銈掋儵銉欍儷銇拷鍔犮仐銇︺generate闁㈡暟銇屽懠銇冲嚭銇曘倢銇熴仺銇嶃伀绨″崢銇搷浣溿仹銇嶃倠銈堛亞銇仚銈嬪繀瑕併亴銇傘倞銇俱仚銆傘亾銇儠銈°偆銉伄ID銇ㄣ佷笂閮ㄣ伄銉°偆銉炽偝銉笺儔銇垾銇甀D銇ㄣ伄闁總銈掋佹銇BoxLayout琛屻伄鐩村墠銇寚瀹氥仚銈嬪繀瑕併亴銇傘倞銇俱仚銆

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15

銇撱伄random_label: random_label琛屻伅鍩烘湰鐨勩伀銆両Drandom_label銈掓寔銇ゃ儵銉欍儷銇屻儠銈°偆銉random_label鍐呫伀銉炪儍銉椼仌銈屻倠銇撱仺銈main.py鎰忓懗銇椼伨銇欍傘仱銇俱倞銆佹搷浣random_label銇欍倠銈€偗銈枫儳銉炽伅銇欍伖銇︺佹寚瀹氥仌銈屻仧鍚嶅墠銇儵銉欍儷銇優銉冦儣銇曘倢銇俱仚銆

銇撱倢銇с併儭銈ゃ兂銉曘偂銈ゃ儷銇贡鏁般倰鐢熸垚銇欍倠銉°偨銉冦儔銈掍綔鎴愩仹銇嶃伨銇欍

def generate_number(self):
    self.random_label.text = str(random.randint(0, 2000))

# notice how the class method manipulates the text attributre of the random label by a# ssigning it a new random number generate by the 'random.randint(0, 2000)' funcion. S# ince this the random number generated is an integer, typecasting is required to make # it a string otherwise you will get a typeError in your terminal when you run it.

MyRoot銇撱伄銈儵銈广伅銆佷互涓嬨伄銈炽兗銉夈伄銈堛亞銇仾銈娿伨銇欍

class MyRoot(BoxLayout):
    def __init__(self):
        super(MyRoot, self).__init__()

    def generate_number(self):
        self.random_label.text = str(random.randint(0, 2000))

銇娿倎銇с仺銇嗭紒銇撱倢銇с併偄銉椼儶銇儭銈ゃ兂銉曘偂銈ゃ儷銇屽畬鎴愩仐銇俱仐銇熴傘亗銇ㄣ伅銆generate銉溿偪銉炽亴銈儶銉冦偗銇曘倢銇熴仺銇嶃伀蹇呫仛銇撱伄闁㈡暟銈掑懠銇冲嚭銇欍倛銇嗐伀銇椼仸銇忋仩銇曘亜銆傘儠銈°偆銉伄on_press: root.generate_number()銉溿偪銉抽伕鎶為儴鍒嗐伀琛屻倰杩藉姞銇欍倠銇犮亼銇ф笀銇.kv銇俱仚銆

<MyRoot>:
    random_label: random_label
    BoxLayout:
        orientation: "vertical"
        Label:
            text: "Random Number"
            font_size: 30
            color: 0, 0.62, 0.96

        Label:
            id: random_label
            text: "_"
            font_size: 30

        Button:
            text: "Generate"
            font_size: 15
            on_press: root.generate_number()

銇撱倢銇с併偄銉椼儶銈掑疅琛屻仹銇嶃伨銇欍

Android銇с偄銉椼儶銈掋偝銉炽儜銈ゃ儷銇欍倠

Android銇с偄銉椼儶銈掋偝銉炽儜銈ゃ儷銇欍倠鍓嶃伀銆乄indows銉︺兗銈躲兗銇仺銇c仸鎮亜銉嬨儱銉笺偣銇屻亗銈娿伨銇欍侫ndroid銈€儣銉偙銉笺偡銉с兂銈掋偝銉炽儜銈ゃ儷銇欍倠銇伅銆丩inux銇俱仧銇痬acOS銇屽繀瑕併仹銇欍傘仧銇犮仐銆佸嬪垾銇甃inux銉囥偅銈广儓銉儞銉ャ兗銈枫儳銉炽倰鐢ㄦ剰銇欍倠蹇呰銇仾銇忋佷唬銈忋倞銇划鎯炽優銈枫兂銈掍娇鐢ㄣ仹銇嶃伨銇欍

瀹屽叏銇狝ndroid.apk銈€儣銉偙銉笺偡銉с兂銈掋偝銉炽儜銈ゃ儷銇椼仸鐢熸垚銇欍倠銇伅銆Buildozer銇ㄣ亜銇嗐儎銉笺儷銈掍娇鐢ㄣ仐銇俱仚銆備互涓嬨伄銈炽優銉炽儔銇亜銇氥倢銇嬨倰浣跨敤銇椼仸銆併偪銉笺儫銉娿儷銇嬨倝Buildozer銈掋偆銉炽偣銉堛兗銉仐銇俱仐銈囥亞銆

pip3 install buildozer
//
pip install buildozer

娆°伀銆丅uildozer銇繀瑕併仾渚濆瓨闁總銇亜銇忋仱銇嬨倰銈ゃ兂銈广儓銉笺儷銇椼伨銇欍傜銇疞inuxErgo銈掍娇鐢ㄣ仐銇︺亜銈嬨伄銇с丩inux鍥烘湁銇偝銉炪兂銉夈倰浣跨敤銇椼伨銇欍傘亾銈屻倝銇偝銉炪兂銉夈倰1銇ゃ仛銇ゅ疅琛屻仚銈嬪繀瑕併亴銇傘倞銇俱仚銆

sudo apt update
sudo apt install -y git zip unzip openjdk-13-jdk python3-pip autoconf libtool pkg-config zlib1g-dev libncurses5-dev libncursesw5-dev libtinfo5 cmake libffi-dev libssl-dev

pip3 install --upgrade Cython==0.29.19 virtualenv 

# add the following line at the end of your ~/.bashrc file
export PATH=$PATH:~/.local/bin/

鐗瑰畾銇偝銉炪兂銉夈倰瀹熻銇椼仧寰屻併倰瀹熻銇buildozer init銇俱仚銆備互涓嬨伄銈广偗銉兗銉炽偡銉с儍銉堛伄銈堛亞銇嚭鍔涖亴琛ㄧず銇曘倢銇俱仚銆

Buildozer銇垵鏈熷寲銇屾垚鍔熴仐銇俱仐銇

涓婅銇偝銉炪兂銉夈伅Buildozer.spec銉曘偂銈ゃ儷銈掍綔鎴愩仐銇俱仚銆傘亾銇儠銈°偆銉倰浣跨敤銇椼仸銆併偄銉椼儶銇悕鍓嶃倓銈€偆銈炽兂銇仼銈掋偄銉椼儶銇寚瀹.spec銇с亶銇俱仚銆傘儠銈°偆銉伅娆°伄銈炽兗銉夈儢銉儍銈伄銈堛亞銇仾銈娿伨銇欍

[app]

# (str) Title of your application
title = My Application

# (str) Package name
package.name = myapp

# (str) Package domain (needed for android/ios packaging)
package.domain = org.test

# (str) Source code where the main.py live
source.dir = .

# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,kv,atlas

# (list) List of inclusions using pattern matching
#source.include_patterns = assets/*,images/*.png

# (list) Source files to exclude (let empty to not exclude anything)
#source.exclude_exts = spec

# (list) List of directory to exclude (let empty to not exclude anything)
#source.exclude_dirs = tests, bin

# (list) List of exclusions using pattern matching
#source.exclude_patterns = license,images/*/*.jpg

# (str) Application versioning (method 1)
version = 0.1

# (str) Application versioning (method 2)
# version.regex = __version__ = \['"\](.*)['"]
# version.filename = %(source.dir)s/main.py

# (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy
requirements = python3,kivy

# (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes
# requirements.source.kivy = ../../kivy

# (list) Garden requirements
#garden_requirements =

# (str) Presplash of the application
#presplash.filename = %(source.dir)s/data/presplash.png

# (str) Icon of the application
#icon.filename = %(source.dir)s/data/icon.png

# (str) Supported orientation (one of landscape, sensorLandscape, portrait or all)
orientation = portrait

# (list) List of service to declare
#services = NAME:ENTRYPOINT_TO_PY,NAME2:ENTRYPOINT2_TO_PY

#
# OSX Specific
#

#
# author = 漏 Copyright Info

# change the major version of python used by the app
osx.python_version = 3

# Kivy version to use
osx.kivy_version = 1.9.1

#
# Android specific
#

# (bool) Indicate if the application should be fullscreen or not
fullscreen = 0

# (string) Presplash background color (for new android toolchain)
# Supported formats are: #RRGGBB #AARRGGBB or one of the following names:
# red, blue, green, black, white, gray, cyan, magenta, yellow, lightgray,
# darkgray, grey, lightgrey, darkgrey, aqua, fuchsia, lime, maroon, navy,
# olive, purple, silver, teal.
#android.presplash_color = #FFFFFF

# (list) Permissions
#android.permissions = INTERNET

# (int) Target Android API, should be as high as possible.
#android.api = 27

# (int) Minimum API your APK will support.
#android.minapi = 21

# (int) Android SDK version to use
#android.sdk = 20

# (str) Android NDK version to use
#android.ndk = 19b

# (int) Android NDK API to use. This is the minimum API your app will support, it should usually match android.minapi.
#android.ndk_api = 21

# (bool) Use --private data storage (True) or --dir public storage (False)
#android.private_storage = True

# (str) Android NDK directory (if empty, it will be automatically downloaded.)
#android.ndk_path =

# (str) Android SDK directory (if empty, it will be automatically downloaded.)
#android.sdk_path =

# (str) ANT directory (if empty, it will be automatically downloaded.)
#android.ant_path =

# (bool) If True, then skip trying to update the Android sdk
# This can be useful to avoid excess Internet downloads or save time
# when an update is due and you just want to test/build your package
# android.skip_update = False

# (bool) If True, then automatically accept SDK license
# agreements. This is intended for automation only. If set to False,
# the default, you will be shown the license when first running
# buildozer.
# android.accept_sdk_license = False

# (str) Android entry point, default is ok for Kivy-based app
#android.entrypoint = org.renpy.android.PythonActivity

# (str) Android app theme, default is ok for Kivy-based app
# android.apptheme = "@android:style/Theme.NoTitleBar"

# (list) Pattern to whitelist for the whole project
#android.whitelist =

# (str) Path to a custom whitelist file
#android.whitelist_src =

# (str) Path to a custom blacklist file
#android.blacklist_src =

# (list) List of Java .jar files to add to the libs so that pyjnius can access
# their classes. Don't add jars that you do not need, since extra jars can slow
# down the build process. Allows wildcards matching, for example:
# OUYA-ODK/libs/*.jar
#android.add_jars = foo.jar,bar.jar,path/to/more/*.jar

# (list) List of Java files to add to the android project (can be java or a
# directory containing the files)
#android.add_src =

# (list) Android AAR archives to add (currently works only with sdl2_gradle
# bootstrap)
#android.add_aars =

# (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap)
#android.gradle_dependencies =

# (list) add java compile options
# this can for example be necessary when importing certain java libraries using the 'android.gradle_dependencies' option
# see https://developer.android.com/studio/write/java8-support for further information
# android.add_compile_options = "sourceCompatibility = 1.8", "targetCompatibility = 1.8"

# (list) Gradle repositories to add {can be necessary for some android.gradle_dependencies}
# please enclose in double quotes 
# e.g. android.gradle_repositories = "maven { url 'https://kotlin.bintray.com/ktor' }"
#android.add_gradle_repositories =

# (list) packaging options to add 
# see https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.PackagingOptions.html
# can be necessary to solve conflicts in gradle_dependencies
# please enclose in double quotes 
# e.g. android.add_packaging_options = "exclude 'META-INF/common.kotlin_module'", "exclude 'META-INF/*.kotlin_module'"
#android.add_gradle_repositories =

# (list) Java classes to add as activities to the manifest.
#android.add_activities = com.example.ExampleActivity

# (str) OUYA Console category. Should be one of GAME or APP
# If you leave this blank, OUYA support will not be enabled
#android.ouya.category = GAME

# (str) Filename of OUYA Console icon. It must be a 732x412 png image.
#android.ouya.icon.filename = %(source.dir)s/data/ouya_icon.png

# (str) XML file to include as an intent filters in <activity> tag
#android.manifest.intent_filters =

# (str) launchMode to set for the main activity
#android.manifest.launch_mode = standard

# (list) Android additional libraries to copy into libs/armeabi
#android.add_libs_armeabi = libs/android/*.so
#android.add_libs_armeabi_v7a = libs/android-v7/*.so
#android.add_libs_arm64_v8a = libs/android-v8/*.so
#android.add_libs_x86 = libs/android-x86/*.so
#android.add_libs_mips = libs/android-mips/*.so

# (bool) Indicate whether the screen should stay on
# Don't forget to add the WAKE_LOCK permission if you set this to True
#android.wakelock = False

# (list) Android application meta-data to set (key=value format)
#android.meta_data =

# (list) Android library project to add (will be added in the
# project.properties automatically.)
#android.library_references =

# (list) Android shared libraries which will be added to AndroidManifest.xml using <uses-library> tag
#android.uses_library =

# (str) Android logcat filters to use
#android.logcat_filters = *:S python:D

# (bool) Copy library instead of making a libpymodules.so
#android.copy_libs = 1

# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
android.arch = armeabi-v7a

# (int) overrides automatic versionCode computation (used in build.gradle)
# this is not the same as app version and should only be edited if you know what you're doing
# android.numeric_version = 1

#
# Python for android (p4a) specific
#

# (str) python-for-android fork to use, defaults to upstream (kivy)
#p4a.fork = kivy

# (str) python-for-android branch to use, defaults to master
#p4a.branch = master

# (str) python-for-android git clone directory (if empty, it will be automatically cloned from github)
#p4a.source_dir =

# (str) The directory in which python-for-android should look for your own build recipes (if any)
#p4a.local_recipes =

# (str) Filename to the hook for p4a
#p4a.hook =

# (str) Bootstrap to use for android builds
# p4a.bootstrap = sdl2

# (int) port number to specify an explicit --port= p4a argument (eg for bootstrap flask)
#p4a.port =


#
# iOS specific
#

# (str) Path to a custom kivy-ios folder
#ios.kivy_ios_dir = ../kivy-ios
# Alternately, specify the URL and branch of a git checkout:
ios.kivy_ios_url = https://github.com/kivy/kivy-ios
ios.kivy_ios_branch = master

# Another platform dependency: ios-deploy
# Uncomment to use a custom checkout
#ios.ios_deploy_dir = ../ios_deploy
# Or specify URL and branch
ios.ios_deploy_url = https://github.com/phonegap/ios-deploy
ios.ios_deploy_branch = 1.7.0

# (str) Name of the certificate to use for signing the debug version
# Get a list of available identities: buildozer ios list_identities
#ios.codesign.debug = "iPhone Developer: <lastname> <firstname> (<hexstring>)"

# (str) Name of the certificate to use for signing the release version
#ios.codesign.release = %(ios.codesign.debug)s


[buildozer]

# (int) Log level (0 = error only, 1 = info, 2 = debug (with command output))
log_level = 2

# (int) Display warning if buildozer is run as root (0 = False, 1 = True)
warn_on_root = 1

# (str) Path to build artifact storage, absolute or relative to spec file
# build_dir = ./.buildozer

# (str) Path to build output (i.e. .apk, .ipa) storage
# bin_dir = ./bin

#    -----------------------------------------------------------------------------
#    List as sections
#
#    You can define all the "list" as [section:key].
#    Each line will be considered as a option to the list.
#    Let's take [app] / source.exclude_patterns.
#    Instead of doing:
#
#[app]
#source.exclude_patterns = license,data/audio/*.wav,data/images/original/*
#
#    This can be translated into:
#
#[app:source.exclude_patterns]
#license
#data/audio/*.wav
#data/images/original/*
#


#    -----------------------------------------------------------------------------
#    Profiles
#
#    You can extend section / key with a profile
#    For example, you want to deploy a demo version of your application without
#    HD content. You could first change the title to add "(demo)" in the name
#    and extend the excluded directories to remove the HD content.
#
#[app@demo]
#title = My Application (demo)
#
#[app:source.exclude_patterns@demo]
#images/hd/*
#
#    Then, invoke the command line with the "demo" profile:
#
#buildozer --profile demo android debug

銈€偆銈炽兂銆佽浠躲併儹銉笺儔鐢婚潰銇仼銈掓寚瀹氥仚銈嬪牬鍚堛伅銆併亾銇儠銈°偆銉倰绶ㄩ泦銇欍倠蹇呰銇屻亗銈娿伨銇欍傘偄銉椼儶銈便兗銈枫儳銉炽伀蹇呰銇仚銇广仸銇法闆嗐倰琛屻仯銇熷緦buildozer -v android debug銆併偄銉椼儶銉囥偅銉偗銉堛儶銇嬨倝瀹熻銇椼仸銆併偄銉椼儶銈便兗銈枫儳銉炽倰銉撱儷銉夈亰銈堛伋銈炽兂銉戙偆銉仐銇俱仚銆傜壒銇綆閫熴伄銉炪偡銉炽倰浣跨敤銇椼仸銇勩倠鍫村悎銇併亾銈屻伀鏅傞枔銇屻亱銇嬨倠銇撱仺銇屻亗銈娿伨銇欍

銉椼儹銈汇偣銇屽畬浜嗐仚銈嬨仺銆佺鏈伀銇勩亸銇ゃ亱銇儹銈般亴琛ㄧず銇曘倢銆併儞銉儔銇屾垚鍔熴仐銇熴亾銇ㄣ倰纰鸿獚銇с亶銇俱仚銆

Android銇垚鍔熴仐銇熴儞銉儔

銇俱仧銆乥in銉囥偅銉偗銉堛儶銇偄銉椼儶銇瓵PK銉愩兗銈搞儳銉炽亴蹇呰銇с仚銆傘亾銈屻伅銆佹惡甯浕瑭便伀銈ゃ兂銈广儓銉笺儷銇椼仸瀹熻銇欍倠銈€儣銉偙銉笺偡銉с兂銇疅琛屽彲鑳姐儠銈°偆銉仹銇欍

bin銉囥偅銉偗銉堛儶銇瓵ndroid.apk

绲愯珫

銇娿倎銇с仺銇嗭紒銇撱伄銉併儱銉笺儓銉偄銉倰銈广儐銉冦儣銉愩偆銈广儐銉冦儣銇у疅琛屻仐銇熷牬鍚堛伅銆侀浕瑭便伀鍗樼磾銇贡鏁般偢銈с儘銉兗銈裤兗銈€儣銉亴銈ゃ兂銈广儓銉笺儷銇曘倢銇︺亜銈嬨伅銇氥仹銇欍傘仢銈屻倰銇勩仒銇c仸銆併亜銇忋仱銇嬨伄鍊ゃ倰寰鏁淬仐銇︺亱銈夈佸啀妲嬬瘔銇椼仸銇忋仩銇曘亜銆傚啀妲嬬瘔銇疅琛屻伅銆佹渶鍒濄伄銉撱儷銉夈伝銇╂檪闁撱伅銇嬨亱銈娿伨銇涖倱銆

銇旇Η銇仺銇娿倞銆丳ython銈掍娇鐢ㄣ仐銇熴儮銉愩偆銉偄銉椼儶銈便兗銈枫儳銉炽伄妲嬬瘔銇佷娇鐢ㄣ仐銇︺亜銈嬨儠銉兗銉犮儻銉笺偗銇俱仧銇儮銈搞儱銉笺儷銇簿閫氥仐銇︺亜銈嬮檺銈娿銇嬨仾銈婄啊鍗銇с仚銆傘仺銇亱銇忋併儹銈搞儍銈伅鍚屻仒鏂规硶銇у疅琛屻仌銈屻伨銇欍

Kivy銉€偢銉ャ兗銉仺銇濄伄銈︺偅銈搞偋銉冦儓銇叄銈屻仸銇忋仩銇曘亜銆傘仚銇广仸銈掍竴搴︺伀鐭ャ倠銇撱仺銇仹銇嶃伨銇涖倱銆傘儣銉偢銈с偗銉堛倰瑕嬨仱銇戙仸銆併仹銇嶃倠銇犮亼鏃┿亸瓒炽倰婵°倝銇欍仩銇戙仹銇欍傘儚銉冦償銉笺偝銉笺儑銈c兂銈般

銉兂銈細https//blog.logrocket.com/build-android-application-kivy-python-framework/

#python 

Plpgsql Check: Extension That Allows to Check Plpgsql Source Code.

plpgsql_check

I founded this project, because I wanted to publish the code I wrote in the last two years, when I tried to write enhanced checking for PostgreSQL upstream. It was not fully successful - integration into upstream requires some larger plpgsql refactoring - probably it will not be done in next years (now is Dec 2013). But written code is fully functional and can be used in production (and it is used in production). So, I created this extension to be available for all plpgsql developers.

If you like it and if you would to join to development of this extension, register yourself to postgresql extension hacking google group.

Features

  • check fields of referenced database objects and types inside embedded SQL
  • using correct types of function parameters
  • unused variables and function argumens, unmodified OUT argumens
  • partially detection of dead code (due RETURN command)
  • detection of missing RETURN command in function
  • try to identify unwanted hidden casts, that can be performance issue like unused indexes
  • possibility to collect relations and functions used by function
  • possibility to check EXECUTE stmt agaist SQL injection vulnerability

I invite any ideas, patches, bugreports.

plpgsql_check is next generation of plpgsql_lint. It allows to check source code by explicit call plpgsql_check_function.

PostgreSQL PostgreSQL 10, 11, 12, 13 and 14 are supported.

The SQL statements inside PL/pgSQL functions are checked by validator for semantic errors. These errors can be found by plpgsql_check_function:

Active mode

postgres=# CREATE EXTENSION plpgsql_check;
LOAD
postgres=# CREATE TABLE t1(a int, b int);
CREATE TABLE

postgres=#
CREATE OR REPLACE FUNCTION public.f1()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE r record;
BEGIN
  FOR r IN SELECT * FROM t1
  LOOP
    RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column
  END LOOP;
END;
$function$;

CREATE FUNCTION

postgres=# select f1(); -- execution doesn't find a bug due to empty table t1
  f1 
 鈹鈹鈹鈹
   
 (1 row)

postgres=# \x
Expanded display is on.
postgres=# select * from plpgsql_check_function_tb('f1()');
鈹[ RECORD 1 ]鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
functionid 鈹 f1
lineno     鈹 6
statement  鈹 RAISE
sqlstate   鈹 42703
message    鈹 record "r" has no field "c"
detail     鈹 [null]
hint       鈹 [null]
level      鈹 error
position   鈹 0
query      鈹 [null]

postgres=# \sf+ f1
    CREATE OR REPLACE FUNCTION public.f1()
     RETURNS void
     LANGUAGE plpgsql
1       AS $function$
2       DECLARE r record;
3       BEGIN
4         FOR r IN SELECT * FROM t1
5         LOOP
6           RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column
7         END LOOP;
8       END;
9       $function$

Function plpgsql_check_function() has three possible formats: text, json or xml

select * from plpgsql_check_function('f1()', fatal_errors := false);
                         plpgsql_check_function                         
------------------------------------------------------------------------
 error:42703:4:SQL statement:column "c" of relation "t1" does not exist
 Query: update t1 set c = 30
 --                   ^
 error:42P01:7:RAISE:missing FROM-clause entry for table "r"
 Query: SELECT r.c
 --            ^
 error:42601:7:RAISE:too few parameters specified for RAISE
(7 rows)

postgres=# select * from plpgsql_check_function('fx()', format:='xml');
                 plpgsql_check_function                     
鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
 <Function oid="16400">                                        鈫
   <Issue>                                                     鈫
     <Level>error</level>                                      鈫
     <Sqlstate>42P01</Sqlstate>                                鈫
     <Message>relation "foo111" does not exist</Message>       鈫
     <Stmt lineno="3">RETURN</Stmt>                            鈫
     <Query position="23">SELECT (select a from foo111)</Query>鈫
   </Issue>                                                    鈫
  </Function>
 (1 row)

Arguments

You can set level of warnings via function's parameters:

Mandatory arguments

  • function name or function signature - these functions requires function specification. Any function in PostgreSQL can be specified by Oid or by name or by signature. When you know oid or complete function's signature, you can use a regprocedure type parameter like 'fx()'::regprocedure or 16799::regprocedure. Possible alternative is using a name only, when function's name is unique - like 'fx'. When the name is not unique or the function doesn't exists it raises a error.

Optional arguments

relid DEFAULT 0 - oid of relation assigned with trigger function. It is necessary for check of any trigger function.

fatal_errors boolean DEFAULT true - stop on first error

other_warnings boolean DEFAULT true - show warnings like different attributes number in assignmenet on left and right side, variable overlaps function's parameter, unused variables, unwanted casting, ..

extra_warnings boolean DEFAULT true - show warnings like missing RETURN, shadowed variables, dead code, never read (unused) function's parameter, unmodified variables, modified auto variables, ..

performance_warnings boolean DEFAULT false - performance related warnings like declared type with type modificator, casting, implicit casts in where clause (can be reason why index is not used), ..

security_warnings boolean DEFAULT false - security related checks like SQL injection vulnerability detection

anyelementtype regtype DEFAULT 'int' - a real type used instead anyelement type

anyenumtype regtype DEFAULT '-' - a real type used instead anyenum type

anyrangetype regtype DEFAULT 'int4range' - a real type used instead anyrange type

anycompatibletype DEFAULT 'int' - a real type used instead anycompatible type

anycompatiblerangetype DEFAULT 'int4range' - a real type used instead anycompatible range type

without_warnings DEFAULT false - disable all warnings

all_warnings DEFAULT false - enable all warnings

newtable DEFAULT NULL, oldtable DEFAULT NULL - the names of NEW or OLD transitive tables. These parameters are required when transitive tables are used.

Triggers

When you want to check any trigger, you have to enter a relation that will be used together with trigger function

CREATE TABLE bar(a int, b int);

postgres=# \sf+ foo_trg
    CREATE OR REPLACE FUNCTION public.foo_trg()
         RETURNS trigger
         LANGUAGE plpgsql
1       AS $function$
2       BEGIN
3         NEW.c := NEW.a + NEW.b;
4         RETURN NEW;
5       END;
6       $function$

Missing relation specification

postgres=# select * from plpgsql_check_function('foo_trg()');
ERROR:  missing trigger relation
HINT:  Trigger relation oid must be valid

Correct trigger checking (with specified relation)

postgres=# select * from plpgsql_check_function('foo_trg()', 'bar');
                 plpgsql_check_function                 
--------------------------------------------------------
 error:42703:3:assignment:record "new" has no field "c"
(1 row)

For triggers with transitive tables you can set a oldtable or newtable parameters:

create or replace function footab_trig_func()
returns trigger as $$
declare x int;
begin
  if false then
    -- should be ok;
    select count(*) from newtab into x; 

    -- should fail;
    select count(*) from newtab where d = 10 into x;
  end if;
  return null;
end;
$$ language plpgsql;

select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab');

Mass check

You can use the plpgsql_check_function for mass check functions and mass check triggers. Please, test following queries:

-- check all nontrigger plpgsql functions
SELECT p.oid, p.proname, plpgsql_check_function(p.oid)
   FROM pg_catalog.pg_namespace n
   JOIN pg_catalog.pg_proc p ON pronamespace = n.oid
   JOIN pg_catalog.pg_language l ON p.prolang = l.oid
  WHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279;

or

SELECT p.proname, tgrelid::regclass, cf.*
   FROM pg_proc p
        JOIN pg_trigger t ON t.tgfoid = p.oid 
        JOIN pg_language l ON p.prolang = l.oid
        JOIN pg_namespace n ON p.pronamespace = n.oid,
        LATERAL plpgsql_check_function(p.oid, t.tgrelid) cf
  WHERE n.nspname = 'public' and l.lanname = 'plpgsql'

or

-- check all plpgsql functions (functions or trigger functions with defined triggers)
SELECT
    (pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement,
    (pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level,
    (pcf)."position", (pcf).query, (pcf).context
FROM
(
    SELECT
        plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0)) AS pcf
    FROM pg_proc
    LEFT JOIN pg_trigger
        ON (pg_trigger.tgfoid = pg_proc.oid)
    WHERE
        prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND
        pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND
        -- ignore unused triggers
        (pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR
         pg_trigger.tgfoid IS NOT NULL)
    OFFSET 0
) ss
ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno

Passive mode

Functions should be checked on start - plpgsql_check module must be loaded.

Configuration

plpgsql_check.mode = [ disabled | by_function | fresh_start | every_start ]
plpgsql_check.fatal_errors = [ yes | no ]

plpgsql_check.show_nonperformance_warnings = false
plpgsql_check.show_performance_warnings = false

Default mode is by_function, that means that the enhanced check is done only in active mode - by plpgsql_check_function. fresh_start means cold start.

You can enable passive mode by

load 'plpgsql'; -- 1.1 and higher doesn't need it
load 'plpgsql_check';
set plpgsql_check.mode = 'every_start';

SELECT fx(10); -- run functions - function is checked before runtime starts it

Limits

plpgsql_check should find almost all errors on really static code. When developer use some PLpgSQL's dynamic features like dynamic SQL or record data type, then false positives are possible. These should be rare - in well written code - and then the affected function should be redesigned or plpgsql_check should be disabled for this function.

CREATE OR REPLACE FUNCTION f1()
RETURNS void AS $$
DECLARE r record;
BEGIN
  FOR r IN EXECUTE 'SELECT * FROM t1'
  LOOP
    RAISE NOTICE '%', r.c;
  END LOOP;
END;
$$ LANGUAGE plpgsql SET plpgsql.enable_check TO false;

A usage of plpgsql_check adds a small overhead (in enabled passive mode) and you should use it only in develop or preprod environments.

Dynamic SQL

This module doesn't check queries that are assembled in runtime. It is not possible to identify results of dynamic queries - so plpgsql_check cannot to set correct type to record variables and cannot to check a dependent SQLs and expressions.

When type of record's variable is not know, you can assign it explicitly with pragma type:

DECLARE r record;
BEGIN
  EXECUTE format('SELECT * FROM %I', _tablename) INTO r;
  PERFORM plpgsql_check_pragma('type: r (id int, processed bool)');
  IF NOT r.processed THEN
    ...

Attention: The SQL injection check can detect only some SQL injection vulnerabilities. This tool cannot be used for security audit! Some issues should not be detected. This check can raise false alarms too - probably when variable is sanitized by other command or when value is of some compose type. 

Refcursors

plpgsql_check should not to detect structure of referenced cursors. A reference on cursor in PLpgSQL is implemented as name of global cursor. In check time, the name is not known (not in all possibilities), and global cursor doesn't exist. It is significant break for any static analyse. PLpgSQL cannot to set correct type for record variables and cannot to check a dependent SQLs and expressions. A solution is same like dynamic SQL. Don't use record variable as target when you use refcursor type or disable plpgsql_check for these functions.

CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
  rec_var record;
BEGIN
  FETCH refcur_var INTO rec_var; -- this is STOP for plpgsql_check
  RAISE NOTICE '%', rec_var;     -- record rec_var is not assigned yet error

In this case a record type should not be used (use known rowtype instead):

CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
  rec_var some_rowtype;
BEGIN
  FETCH refcur_var INTO rec_var;
  RAISE NOTICE '%', rec_var;

Temporary tables

plpgsql_check cannot verify queries over temporary tables that are created in plpgsql's function runtime. For this use case it is necessary to create a fake temp table or disable plpgsql_check for this function.

In reality temp tables are stored in own (per user) schema with higher priority than persistent tables. So you can do (with following trick safetly):

CREATE OR REPLACE FUNCTION public.disable_dml()
RETURNS trigger
LANGUAGE plpgsql AS $function$
BEGIN
  RAISE EXCEPTION SQLSTATE '42P01'
     USING message = format('this instance of %I table doesn''t allow any DML operation', TG_TABLE_NAME),
           hint = format('you should to run "CREATE TEMP TABLE %1$I(LIKE %1$I INCLUDING ALL);" statement',
                         TG_TABLE_NAME);
  RETURN NULL;
END;
$function$;

CREATE TABLE foo(a int, b int); -- doesn't hold data ever
CREATE TRIGGER foo_disable_dml
   BEFORE INSERT OR UPDATE OR DELETE ON foo
   EXECUTE PROCEDURE disable_dml();

postgres=# INSERT INTO  foo VALUES(10,20);
ERROR:  this instance of foo table doesn't allow any DML operation
HINT:  you should to run "CREATE TEMP TABLE foo(LIKE foo INCLUDING ALL);" statement
postgres=# 

CREATE TABLE
postgres=# INSERT INTO  foo VALUES(10,20);
INSERT 0 1

This trick emulates GLOBAL TEMP tables partially and it allows a statical validation. Other possibility is using a [template foreign data wrapper] (https://github.com/okbob/template_fdw)

You can use pragma table and create ephemeral table:

BEGIN
   CREATE TEMP TABLE xxx(a int);
   PERFORM plpgsql_check_pragma('table: xxx(a int)');
   INSERT INTO xxx VALUES(10);

Dependency list

A function plpgsql_show_dependency_tb can show all functions, operators and relations used inside processed function:

postgres=# select * from plpgsql_show_dependency_tb('testfunc(int,float)');
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹   type   鈹  oid  鈹 schema 鈹  name   鈹           params           鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹 FUNCTION 鈹 36008 鈹 public 鈹 myfunc1 鈹 (integer,double precision) 鈹
鈹 FUNCTION 鈹 35999 鈹 public 鈹 myfunc2 鈹 (integer,double precision) 鈹
鈹 OPERATOR 鈹 36007 鈹 public 鈹 **      鈹 (integer,integer)          鈹
鈹 RELATION 鈹 36005 鈹 public 鈹 myview  鈹                            鈹
鈹 RELATION 鈹 36002 鈹 public 鈹 mytable 鈹                            鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(4 rows)

Profiler

The plpgsql_check contains simple profiler of plpgsql functions and procedures. It can work with/without a access to shared memory. It depends on shared_preload_libraries config. When plpgsql_check was initialized by shared_preload_libraries, then it can allocate shared memory, and function's profiles are stored there. When plpgsql_check cannot to allocate shared momory, the profile is stored in session memory.

Due dependencies, shared_preload_libraries should to contains plpgsql first

postgres=# show shared_preload_libraries ;
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹 shared_preload_libraries 鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹 plpgsql,plpgsql_check    鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(1 row)

The profiler is active when GUC plpgsql_check.profiler is on. The profiler doesn't require shared memory, but if there are not shared memory, then the profile is limmitted just to active session.

When plpgsql_check is initialized by shared_preload_libraries, another GUC is available to configure the amount of shared memory used by the profiler: plpgsql_check.profiler_max_shared_chunks. This defines the maximum number of statements chunk that can be stored in shared memory. For each plpgsql function (or procedure), the whole content is split into chunks of 30 statements. If needed, multiple chunks can be used to store the whole content of a single function. A single chunk is 1704 bytes. The default value for this GUC is 15000, which should be enough for big projects containing hundred of thousands of statements in plpgsql, and will consume about 24MB of memory. If your project doesn't require that much number of chunks, you can set this parameter to a smaller number in order to decrease the memory usage. The minimum value is 50 (which should consume about 83kB of memory), and the maximum value is 100000 (which should consume about 163MB of memory). Changing this parameter requires a PostgreSQL restart.

The profiler will also retrieve the query identifier for each instruction that contains an expression or optimizable statement. Note that this requires pg_stat_statements, or another similar third-party extension), to be installed. There are some limitations to the query identifier retrieval:

  • if a plpgsql expression contains underlying statements, only the top level query identifier will be retrieved
  • the profiler doesn't compute query identifier by itself but relies on external extension, such as pg_stat_statements, for that. It means that depending on the external extension behavior, you may not be able to see a query identifier for some statements. That's for instance the case with DDL statements, as pg_stat_statements doesn't expose the query identifier for such queries.
  • a query identifier is retrieved only for instructions containing expressions. This means that plpgsql_profiler_function_tb() function can report less query identifier than instructions on a single line.

Attention: A update of shared profiles can decrease performance on servers under higher load.

The profile can be displayed by function plpgsql_profiler_function_tb:

postgres=# select lineno, avg_time, source from plpgsql_profiler_function_tb('fx(int)');
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹 lineno 鈹 avg_time 鈹                              source                               鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺
鈹      1 鈹          鈹                                                                   鈹
鈹      2 鈹          鈹 declare result int = 0;                                           鈹
鈹      3 鈹    0.075 鈹 begin                                                             鈹
鈹      4 鈹    0.202 鈹   for i in 1..$1 loop                                             鈹
鈹      5 鈹    0.005 鈹     select result + i into result; select result + i into result; 鈹
鈹      6 鈹          鈹   end loop;                                                       鈹
鈹      7 鈹        0 鈹   return result;                                                  鈹
鈹      8 鈹          鈹 end;                                                              鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(9 rows)

The profile per statements (not per line) can be displayed by function plpgsql_profiler_function_statements_tb:

        CREATE OR REPLACE FUNCTION public.fx1(a integer)
         RETURNS integer
         LANGUAGE plpgsql
1       AS $function$
2       begin
3         if a > 10 then
4           raise notice 'ahoj';
5           return -1;
6         else
7           raise notice 'nazdar';
8           return 1;
9         end if;
10      end;
11      $function$

postgres=# select stmtid, parent_stmtid, parent_note, lineno, exec_stmts, stmtname
             from plpgsql_profiler_function_statements_tb('fx1');
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹 stmtid 鈹 parent_stmtid 鈹 parent_note 鈹 lineno 鈹 exec_stmts 鈹    stmtname     鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹      0 鈹             鈭 鈹 鈭           鈹      2 鈹          0 鈹 statement block 鈹
鈹      1 鈹             0 鈹 body        鈹      3 鈹          0 鈹 IF              鈹
鈹      2 鈹             1 鈹 then body   鈹      4 鈹          0 鈹 RAISE           鈹
鈹      3 鈹             1 鈹 then body   鈹      5 鈹          0 鈹 RETURN          鈹
鈹      4 鈹             1 鈹 else body   鈹      7 鈹          0 鈹 RAISE           鈹
鈹      5 鈹             1 鈹 else body   鈹      8 鈹          0 鈹 RETURN          鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(6 rows)

All stored profiles can be displayed by calling function plpgsql_profiler_functions_all:

postgres=# select * from plpgsql_profiler_functions_all();
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹        funcoid        鈹 exec_count 鈹 total_time 鈹 avg_time 鈹 stddev_time 鈹 min_time 鈹 max_time 鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹 fxx(double precision) 鈹          1 鈹       0.01 鈹     0.01 鈹        0.00 鈹     0.01 鈹     0.01 鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(1 row)

There are two functions for cleaning stored profiles: plpgsql_profiler_reset_all() and plpgsql_profiler_reset(regprocedure).

Coverage metrics

plpgsql_check provides two functions:

  • plpgsql_coverage_statements(name)
  • plpgsql_coverage_branches(name)

Note

There is another very good PLpgSQL profiler - https://bitbucket.org/openscg/plprofiler

My extension is designed to be simple for use and practical. Nothing more or less.

plprofiler is more complex. It build call graphs and from this graph it can creates flame graph of execution times.

Both extensions can be used together with buildin PostgreSQL's feature - tracking functions.

set track_functions to 'pl';
...
select * from pg_stat_user_functions;

Tracer

plpgsql_check provides a tracing possibility - in this mode you can see notices on start or end functions (terse and default verbosity) and start or end statements (verbose verbosity). For default and verbose verbosity the content of function arguments is displayed. The content of related variables are displayed when verbosity is verbose.

postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #0 ->> start of inline_code_block (Oid=0)
NOTICE:  #2   ->> start of function fx(integer,integer,date,text) (Oid=16405)
NOTICE:  #2        call by inline_code_block line 1 at PERFORM
NOTICE:  #2       "a" => '10', "b" => null, "c" => '2020-08-03', "d" => 'st臎hule'
NOTICE:  #4     ->> start of function fx(integer) (Oid=16404)
NOTICE:  #4          call by fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:  #4         "a" => '10'
NOTICE:  #4     <<- end of function fx (elapsed time=0.098 ms)
NOTICE:  #2   <<- end of function fx (elapsed time=0.399 ms)
NOTICE:  #0 <<- end of block (elapsed time=0.754 ms)

The number after # is a execution frame counter (this number is related to deep of error context stack). It allows to pair start end and of function.

Tracing is enabled by setting plpgsql_check.tracer to on. Attention - enabling this behaviour has significant negative impact on performance (unlike the profiler). You can set a level for output used by tracer plpgsql_check.tracer_errlevel (default is notice). The output content is limited by length specified by plpgsql_check.tracer_variable_max_length configuration variable.

In terse verbose mode the output is reduced:

postgres=# set plpgsql_check.tracer_verbosity TO terse;
SET
postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #0 start of inline code block (oid=0)
NOTICE:  #2 start of fx (oid=16405)
NOTICE:  #4 start of fx (oid=16404)
NOTICE:  #4 end of fx
NOTICE:  #2 end of fx
NOTICE:  #0 end of inline code block

In verbose mode the output is extended about statement details:

postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #0            ->> start of block inline_code_block (oid=0)
NOTICE:  #0.1       1  --> start of PERFORM
NOTICE:  #2              ->> start of function fx(integer,integer,date,text) (oid=16405)
NOTICE:  #2                   call by inline_code_block line 1 at PERFORM
NOTICE:  #2                  "a" => '10', "b" => null, "c" => '2020-08-04', "d" => 'st臎hule'
NOTICE:  #2.1       1    --> start of PERFORM
NOTICE:  #2.1                "a" => '10'
NOTICE:  #4                ->> start of function fx(integer) (oid=16404)
NOTICE:  #4                     call by fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:  #4                    "a" => '10'
NOTICE:  #4.1       6      --> start of assignment
NOTICE:  #4.1                  "a" => '10', "b" => '20'
NOTICE:  #4.1              <-- end of assignment (elapsed time=0.076 ms)
NOTICE:  #4.1                  "res" => '130'
NOTICE:  #4.2       7      --> start of RETURN
NOTICE:  #4.2                  "res" => '130'
NOTICE:  #4.2              <-- end of RETURN (elapsed time=0.054 ms)
NOTICE:  #4                <<- end of function fx (elapsed time=0.373 ms)
NOTICE:  #2.1            <-- end of PERFORM (elapsed time=0.589 ms)
NOTICE:  #2              <<- end of function fx (elapsed time=0.727 ms)
NOTICE:  #0.1          <-- end of PERFORM (elapsed time=1.147 ms)
NOTICE:  #0            <<- end of block (elapsed time=1.286 ms)

Special feature of tracer is tracing of ASSERT statement when plpgsql_check.trace_assert is on. When plpgsql_check.trace_assert_verbosity is DEFAULT, then all function's or procedure's variables are displayed when assert expression is false. When this configuration is VERBOSE then all variables from all plpgsql frames are displayed. This behaviour is independent on plpgsql.check_asserts value. It can be used, although the assertions are disabled in plpgsql runtime.

postgres=# set plpgsql_check.tracer to off;
postgres=# set plpgsql_check.trace_assert_verbosity TO verbose;

postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false
NOTICE:   "a" => '10', "res" => null, "b" => '20'
NOTICE:  #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:   "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'st臎hule'
NOTICE:  #0 PL/pgSQL function inline_code_block line 1 at PERFORM
ERROR:  assertion failed
CONTEXT:  PL/pgSQL function fx(integer) line 12 at ASSERT
SQL statement "SELECT fx(a)"
PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
SQL statement "SELECT fx(10,null, 'now', e'st臎hule')"
PL/pgSQL function inline_code_block line 1 at PERFORM

postgres=# set plpgsql.check_asserts to off;
SET
postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false
NOTICE:   "a" => '10', "res" => null, "b" => '20'
NOTICE:  #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:   "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'st臎hule'
NOTICE:  #0 PL/pgSQL function inline_code_block line 1 at PERFORM
DO

Attention - SECURITY

Tracer prints content of variables or function arguments. For security definer function, this content can hold security sensitive data. This is reason why tracer is disabled by default and should be enabled only with super user rights plpgsql_check.enable_tracer.

Pragma

You can configure plpgsql_check behave inside checked function with "pragma" function. This is a analogy of PL/SQL or ADA language of PRAGMA feature. PLpgSQL doesn't support PRAGMA, but plpgsql_check detects function named plpgsql_check_pragma and get options from parameters of this function. These plpgsql_check options are valid to end of group of statements.

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
BEGIN
  ...
  -- for following statements disable check
  PERFORM plpgsql_check_pragma('disable:check');
  ...
  -- enable check again
  PERFORM plpgsql_check_pragma('enable:check');
  ...
END;
$$ LANGUAGE plpgsql;

The function plpgsql_check_pragma is immutable function that returns one. It is defined by plpgsql_check extension. You can declare alternative plpgsql_check_pragma function like:

CREATE OR REPLACE FUNCTION plpgsql_check_pragma(VARIADIC args[])
RETURNS int AS $$
SELECT 1
$$ LANGUAGE sql IMMUTABLE;

Using pragma function in declaration part of top block sets options on function level too.

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
DECLARE
  aux int := plpgsql_check_pragma('disable:extra_warnings');
  ...

Shorter syntax for pragma is supported too:

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
DECLARE r record;
BEGIN
  PERFORM 'PRAGMA:TYPE:r (a int, b int)';
  PERFORM 'PRAGMA:TABLE: x (like pg_class)';
  ...

Supported pragmas

echo:str - print string (for testing)

status:check,status:tracer, status:other_warnings, status:performance_warnings, status:extra_warnings,status:security_warnings

enable:check,enable:tracer, enable:other_warnings, enable:performance_warnings, enable:extra_warnings,enable:security_warnings

disable:check,disable:tracer, disable:other_warnings, disable:performance_warnings, disable:extra_warnings,disable:security_warnings

type:varname typename or type:varname (fieldname type, ...) - set type to variable of record type

table: name (column_name type, ...) or table: name (like tablename) - create ephereal table

Pragmas enable:tracer and disable:tracerare active for Postgres 12 and higher

Compilation

You need a development environment for PostgreSQL extensions:

make clean
make install

result:

[pavel@localhost plpgsql_check]$ make USE_PGXS=1 clean
rm -f plpgsql_check.so   libplpgsql_check.a  libplpgsql_check.pc
rm -f plpgsql_check.o
rm -rf results/ regression.diffs regression.out tmp_check/ log/
[pavel@localhost plpgsql_check]$ make USE_PGXS=1 all
clang -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fpic -I/usr/local/pgsql/lib/pgxs/src/makefiles/../../src/pl/plpgsql/src -I. -I./ -I/usr/local/pgsql/include/server -I/usr/local/pgsql/include/internal -D_GNU_SOURCE   -c -o plpgsql_check.o plpgsql_check.c
clang -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fpic -I/usr/local/pgsql/lib/pgxs/src/makefiles/../../src/pl/plpgsql/src -shared -o plpgsql_check.so plpgsql_check.o -L/usr/local/pgsql/lib -Wl,--as-needed -Wl,-rpath,'/usr/local/pgsql/lib',--enable-new-dtags  
[pavel@localhost plpgsql_check]$ su root
Password: *******
[root@localhost plpgsql_check]# make USE_PGXS=1 install
/usr/bin/mkdir -p '/usr/local/pgsql/lib'
/usr/bin/mkdir -p '/usr/local/pgsql/share/extension'
/usr/bin/mkdir -p '/usr/local/pgsql/share/extension'
/usr/bin/install -c -m 755  plpgsql_check.so '/usr/local/pgsql/lib/plpgsql_check.so'
/usr/bin/install -c -m 644 plpgsql_check.control '/usr/local/pgsql/share/extension/'
/usr/bin/install -c -m 644 plpgsql_check--0.9.sql '/usr/local/pgsql/share/extension/'
[root@localhost plpgsql_check]# exit
[pavel@localhost plpgsql_check]$ make USE_PGXS=1 installcheck
/usr/local/pgsql/lib/pgxs/src/makefiles/../../src/test/regress/pg_regress --inputdir=./ --psqldir='/usr/local/pgsql/bin'    --dbname=pl_regression --load-language=plpgsql --dbname=contrib_regression plpgsql_check_passive plpgsql_check_active plpgsql_check_active-9.5
(using postmaster on Unix socket, default port)
============== dropping database "contrib_regression" ==============
DROP DATABASE
============== creating database "contrib_regression" ==============
CREATE DATABASE
ALTER DATABASE
============== installing plpgsql                     ==============
CREATE LANGUAGE
============== running regression test queries        ==============
test plpgsql_check_passive    ... ok
test plpgsql_check_active     ... ok
test plpgsql_check_active-9.5 ... ok

=====================
 All 3 tests passed. 
=====================

Compilation on Ubuntu

Sometimes successful compilation can require libicu-dev package (PostgreSQL 10 and higher - when pg was compiled with ICU support)

sudo apt install libicu-dev

Compilation plpgsql_check on Windows

You can check precompiled dll libraries http://okbob.blogspot.cz/2015/02/plpgsqlcheck-is-available-for-microsoft.html

or compile by self:

  1. Download and install PostgreSQL for Win32 from http://www.enterprisedb.com
  2. Download and install Microsoft Visual C++ Express
  3. Lern tutorial http://blog.2ndquadrant.com/compiling-postgresql-extensions-visual-studio-windows
  4. Build plpgsql_check.dll
  5. Install plugin
  6. copy plpgsql_check.dll to PostgreSQL\14\lib
  7. copy plpgsql_check.control and plpgsql_check--2.1.sql to PostgreSQL\14\share\extension

Checked on

  • gcc on Linux (against all supported PostgreSQL)
  • clang 3.4 on Linux (against PostgreSQL 10)
  • for success regress tests the PostgreSQL 10 or higher is required

Compilation against PostgreSQL 10 requires libICU!

Licence

Copyright (c) Pavel Stehule (pavel.stehule@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Note

If you like it, send a postcard to address

Pavel Stehule
Skalice 12
256 01 Benesov u Prahy
Czech Republic

I invite any questions, comments, bug reports, patches on mail address pavel.stehule@gmail.com


Author: okbob
Source Code: https://github.com/okbob/plpgsql_check
License: View license

#postgresql 

Franz  Becker

Franz Becker

1648803600

Plpgsql Check: Extension That Allows to Check Plpgsql Source Code.

plpgsql_check

I founded this project, because I wanted to publish the code I wrote in the last two years, when I tried to write enhanced checking for PostgreSQL upstream. It was not fully successful - integration into upstream requires some larger plpgsql refactoring - probably it will not be done in next years (now is Dec 2013). But written code is fully functional and can be used in production (and it is used in production). So, I created this extension to be available for all plpgsql developers.

If you like it and if you would to join to development of this extension, register yourself to postgresql extension hacking google group.

Features

  • check fields of referenced database objects and types inside embedded SQL
  • using correct types of function parameters
  • unused variables and function argumens, unmodified OUT argumens
  • partially detection of dead code (due RETURN command)
  • detection of missing RETURN command in function
  • try to identify unwanted hidden casts, that can be performance issue like unused indexes
  • possibility to collect relations and functions used by function
  • possibility to check EXECUTE stmt agaist SQL injection vulnerability

I invite any ideas, patches, bugreports.

plpgsql_check is next generation of plpgsql_lint. It allows to check source code by explicit call plpgsql_check_function.

PostgreSQL PostgreSQL 10, 11, 12, 13 and 14 are supported.

The SQL statements inside PL/pgSQL functions are checked by validator for semantic errors. These errors can be found by plpgsql_check_function:

Active mode

postgres=# CREATE EXTENSION plpgsql_check;
LOAD
postgres=# CREATE TABLE t1(a int, b int);
CREATE TABLE

postgres=#
CREATE OR REPLACE FUNCTION public.f1()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE r record;
BEGIN
  FOR r IN SELECT * FROM t1
  LOOP
    RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column
  END LOOP;
END;
$function$;

CREATE FUNCTION

postgres=# select f1(); -- execution doesn't find a bug due to empty table t1
  f1 
 鈹鈹鈹鈹
   
 (1 row)

postgres=# \x
Expanded display is on.
postgres=# select * from plpgsql_check_function_tb('f1()');
鈹[ RECORD 1 ]鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
functionid 鈹 f1
lineno     鈹 6
statement  鈹 RAISE
sqlstate   鈹 42703
message    鈹 record "r" has no field "c"
detail     鈹 [null]
hint       鈹 [null]
level      鈹 error
position   鈹 0
query      鈹 [null]

postgres=# \sf+ f1
    CREATE OR REPLACE FUNCTION public.f1()
     RETURNS void
     LANGUAGE plpgsql
1       AS $function$
2       DECLARE r record;
3       BEGIN
4         FOR r IN SELECT * FROM t1
5         LOOP
6           RAISE NOTICE '%', r.c; -- there is bug - table t1 missing "c" column
7         END LOOP;
8       END;
9       $function$

Function plpgsql_check_function() has three possible formats: text, json or xml

select * from plpgsql_check_function('f1()', fatal_errors := false);
                         plpgsql_check_function                         
------------------------------------------------------------------------
 error:42703:4:SQL statement:column "c" of relation "t1" does not exist
 Query: update t1 set c = 30
 --                   ^
 error:42P01:7:RAISE:missing FROM-clause entry for table "r"
 Query: SELECT r.c
 --            ^
 error:42601:7:RAISE:too few parameters specified for RAISE
(7 rows)

postgres=# select * from plpgsql_check_function('fx()', format:='xml');
                 plpgsql_check_function                     
鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
 <Function oid="16400">                                        鈫
   <Issue>                                                     鈫
     <Level>error</level>                                      鈫
     <Sqlstate>42P01</Sqlstate>                                鈫
     <Message>relation "foo111" does not exist</Message>       鈫
     <Stmt lineno="3">RETURN</Stmt>                            鈫
     <Query position="23">SELECT (select a from foo111)</Query>鈫
   </Issue>                                                    鈫
  </Function>
 (1 row)

Arguments

You can set level of warnings via function's parameters:

Mandatory arguments

  • function name or function signature - these functions requires function specification. Any function in PostgreSQL can be specified by Oid or by name or by signature. When you know oid or complete function's signature, you can use a regprocedure type parameter like 'fx()'::regprocedure or 16799::regprocedure. Possible alternative is using a name only, when function's name is unique - like 'fx'. When the name is not unique or the function doesn't exists it raises a error.

Optional arguments

relid DEFAULT 0 - oid of relation assigned with trigger function. It is necessary for check of any trigger function.

fatal_errors boolean DEFAULT true - stop on first error

other_warnings boolean DEFAULT true - show warnings like different attributes number in assignmenet on left and right side, variable overlaps function's parameter, unused variables, unwanted casting, ..

extra_warnings boolean DEFAULT true - show warnings like missing RETURN, shadowed variables, dead code, never read (unused) function's parameter, unmodified variables, modified auto variables, ..

performance_warnings boolean DEFAULT false - performance related warnings like declared type with type modificator, casting, implicit casts in where clause (can be reason why index is not used), ..

security_warnings boolean DEFAULT false - security related checks like SQL injection vulnerability detection

anyelementtype regtype DEFAULT 'int' - a real type used instead anyelement type

anyenumtype regtype DEFAULT '-' - a real type used instead anyenum type

anyrangetype regtype DEFAULT 'int4range' - a real type used instead anyrange type

anycompatibletype DEFAULT 'int' - a real type used instead anycompatible type

anycompatiblerangetype DEFAULT 'int4range' - a real type used instead anycompatible range type

without_warnings DEFAULT false - disable all warnings

all_warnings DEFAULT false - enable all warnings

newtable DEFAULT NULL, oldtable DEFAULT NULL - the names of NEW or OLD transitive tables. These parameters are required when transitive tables are used.

Triggers

When you want to check any trigger, you have to enter a relation that will be used together with trigger function

CREATE TABLE bar(a int, b int);

postgres=# \sf+ foo_trg
    CREATE OR REPLACE FUNCTION public.foo_trg()
         RETURNS trigger
         LANGUAGE plpgsql
1       AS $function$
2       BEGIN
3         NEW.c := NEW.a + NEW.b;
4         RETURN NEW;
5       END;
6       $function$

Missing relation specification

postgres=# select * from plpgsql_check_function('foo_trg()');
ERROR:  missing trigger relation
HINT:  Trigger relation oid must be valid

Correct trigger checking (with specified relation)

postgres=# select * from plpgsql_check_function('foo_trg()', 'bar');
                 plpgsql_check_function                 
--------------------------------------------------------
 error:42703:3:assignment:record "new" has no field "c"
(1 row)

For triggers with transitive tables you can set a oldtable or newtable parameters:

create or replace function footab_trig_func()
returns trigger as $$
declare x int;
begin
  if false then
    -- should be ok;
    select count(*) from newtab into x; 

    -- should fail;
    select count(*) from newtab where d = 10 into x;
  end if;
  return null;
end;
$$ language plpgsql;

select * from plpgsql_check_function('footab_trig_func','footab', newtable := 'newtab');

Mass check

You can use the plpgsql_check_function for mass check functions and mass check triggers. Please, test following queries:

-- check all nontrigger plpgsql functions
SELECT p.oid, p.proname, plpgsql_check_function(p.oid)
   FROM pg_catalog.pg_namespace n
   JOIN pg_catalog.pg_proc p ON pronamespace = n.oid
   JOIN pg_catalog.pg_language l ON p.prolang = l.oid
  WHERE l.lanname = 'plpgsql' AND p.prorettype <> 2279;

or

SELECT p.proname, tgrelid::regclass, cf.*
   FROM pg_proc p
        JOIN pg_trigger t ON t.tgfoid = p.oid 
        JOIN pg_language l ON p.prolang = l.oid
        JOIN pg_namespace n ON p.pronamespace = n.oid,
        LATERAL plpgsql_check_function(p.oid, t.tgrelid) cf
  WHERE n.nspname = 'public' and l.lanname = 'plpgsql'

or

-- check all plpgsql functions (functions or trigger functions with defined triggers)
SELECT
    (pcf).functionid::regprocedure, (pcf).lineno, (pcf).statement,
    (pcf).sqlstate, (pcf).message, (pcf).detail, (pcf).hint, (pcf).level,
    (pcf)."position", (pcf).query, (pcf).context
FROM
(
    SELECT
        plpgsql_check_function_tb(pg_proc.oid, COALESCE(pg_trigger.tgrelid, 0)) AS pcf
    FROM pg_proc
    LEFT JOIN pg_trigger
        ON (pg_trigger.tgfoid = pg_proc.oid)
    WHERE
        prolang = (SELECT lang.oid FROM pg_language lang WHERE lang.lanname = 'plpgsql') AND
        pronamespace <> (SELECT nsp.oid FROM pg_namespace nsp WHERE nsp.nspname = 'pg_catalog') AND
        -- ignore unused triggers
        (pg_proc.prorettype <> (SELECT typ.oid FROM pg_type typ WHERE typ.typname = 'trigger') OR
         pg_trigger.tgfoid IS NOT NULL)
    OFFSET 0
) ss
ORDER BY (pcf).functionid::regprocedure::text, (pcf).lineno

Passive mode

Functions should be checked on start - plpgsql_check module must be loaded.

Configuration

plpgsql_check.mode = [ disabled | by_function | fresh_start | every_start ]
plpgsql_check.fatal_errors = [ yes | no ]

plpgsql_check.show_nonperformance_warnings = false
plpgsql_check.show_performance_warnings = false

Default mode is by_function, that means that the enhanced check is done only in active mode - by plpgsql_check_function. fresh_start means cold start.

You can enable passive mode by

load 'plpgsql'; -- 1.1 and higher doesn't need it
load 'plpgsql_check';
set plpgsql_check.mode = 'every_start';

SELECT fx(10); -- run functions - function is checked before runtime starts it

Limits

plpgsql_check should find almost all errors on really static code. When developer use some PLpgSQL's dynamic features like dynamic SQL or record data type, then false positives are possible. These should be rare - in well written code - and then the affected function should be redesigned or plpgsql_check should be disabled for this function.

CREATE OR REPLACE FUNCTION f1()
RETURNS void AS $$
DECLARE r record;
BEGIN
  FOR r IN EXECUTE 'SELECT * FROM t1'
  LOOP
    RAISE NOTICE '%', r.c;
  END LOOP;
END;
$$ LANGUAGE plpgsql SET plpgsql.enable_check TO false;

A usage of plpgsql_check adds a small overhead (in enabled passive mode) and you should use it only in develop or preprod environments.

Dynamic SQL

This module doesn't check queries that are assembled in runtime. It is not possible to identify results of dynamic queries - so plpgsql_check cannot to set correct type to record variables and cannot to check a dependent SQLs and expressions.

When type of record's variable is not know, you can assign it explicitly with pragma type:

DECLARE r record;
BEGIN
  EXECUTE format('SELECT * FROM %I', _tablename) INTO r;
  PERFORM plpgsql_check_pragma('type: r (id int, processed bool)');
  IF NOT r.processed THEN
    ...

Attention: The SQL injection check can detect only some SQL injection vulnerabilities. This tool cannot be used for security audit! Some issues should not be detected. This check can raise false alarms too - probably when variable is sanitized by other command or when value is of some compose type. 

Refcursors

plpgsql_check should not to detect structure of referenced cursors. A reference on cursor in PLpgSQL is implemented as name of global cursor. In check time, the name is not known (not in all possibilities), and global cursor doesn't exist. It is significant break for any static analyse. PLpgSQL cannot to set correct type for record variables and cannot to check a dependent SQLs and expressions. A solution is same like dynamic SQL. Don't use record variable as target when you use refcursor type or disable plpgsql_check for these functions.

CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
  rec_var record;
BEGIN
  FETCH refcur_var INTO rec_var; -- this is STOP for plpgsql_check
  RAISE NOTICE '%', rec_var;     -- record rec_var is not assigned yet error

In this case a record type should not be used (use known rowtype instead):

CREATE OR REPLACE FUNCTION foo(refcur_var refcursor)
RETURNS void AS $$
DECLARE
  rec_var some_rowtype;
BEGIN
  FETCH refcur_var INTO rec_var;
  RAISE NOTICE '%', rec_var;

Temporary tables

plpgsql_check cannot verify queries over temporary tables that are created in plpgsql's function runtime. For this use case it is necessary to create a fake temp table or disable plpgsql_check for this function.

In reality temp tables are stored in own (per user) schema with higher priority than persistent tables. So you can do (with following trick safetly):

CREATE OR REPLACE FUNCTION public.disable_dml()
RETURNS trigger
LANGUAGE plpgsql AS $function$
BEGIN
  RAISE EXCEPTION SQLSTATE '42P01'
     USING message = format('this instance of %I table doesn''t allow any DML operation', TG_TABLE_NAME),
           hint = format('you should to run "CREATE TEMP TABLE %1$I(LIKE %1$I INCLUDING ALL);" statement',
                         TG_TABLE_NAME);
  RETURN NULL;
END;
$function$;

CREATE TABLE foo(a int, b int); -- doesn't hold data ever
CREATE TRIGGER foo_disable_dml
   BEFORE INSERT OR UPDATE OR DELETE ON foo
   EXECUTE PROCEDURE disable_dml();

postgres=# INSERT INTO  foo VALUES(10,20);
ERROR:  this instance of foo table doesn't allow any DML operation
HINT:  you should to run "CREATE TEMP TABLE foo(LIKE foo INCLUDING ALL);" statement
postgres=# 

CREATE TABLE
postgres=# INSERT INTO  foo VALUES(10,20);
INSERT 0 1

This trick emulates GLOBAL TEMP tables partially and it allows a statical validation. Other possibility is using a [template foreign data wrapper] (https://github.com/okbob/template_fdw)

You can use pragma table and create ephemeral table:

BEGIN
   CREATE TEMP TABLE xxx(a int);
   PERFORM plpgsql_check_pragma('table: xxx(a int)');
   INSERT INTO xxx VALUES(10);

Dependency list

A function plpgsql_show_dependency_tb can show all functions, operators and relations used inside processed function:

postgres=# select * from plpgsql_show_dependency_tb('testfunc(int,float)');
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹   type   鈹  oid  鈹 schema 鈹  name   鈹           params           鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹 FUNCTION 鈹 36008 鈹 public 鈹 myfunc1 鈹 (integer,double precision) 鈹
鈹 FUNCTION 鈹 35999 鈹 public 鈹 myfunc2 鈹 (integer,double precision) 鈹
鈹 OPERATOR 鈹 36007 鈹 public 鈹 **      鈹 (integer,integer)          鈹
鈹 RELATION 鈹 36005 鈹 public 鈹 myview  鈹                            鈹
鈹 RELATION 鈹 36002 鈹 public 鈹 mytable 鈹                            鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(4 rows)

Profiler

The plpgsql_check contains simple profiler of plpgsql functions and procedures. It can work with/without a access to shared memory. It depends on shared_preload_libraries config. When plpgsql_check was initialized by shared_preload_libraries, then it can allocate shared memory, and function's profiles are stored there. When plpgsql_check cannot to allocate shared momory, the profile is stored in session memory.

Due dependencies, shared_preload_libraries should to contains plpgsql first

postgres=# show shared_preload_libraries ;
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹 shared_preload_libraries 鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹 plpgsql,plpgsql_check    鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(1 row)

The profiler is active when GUC plpgsql_check.profiler is on. The profiler doesn't require shared memory, but if there are not shared memory, then the profile is limmitted just to active session.

When plpgsql_check is initialized by shared_preload_libraries, another GUC is available to configure the amount of shared memory used by the profiler: plpgsql_check.profiler_max_shared_chunks. This defines the maximum number of statements chunk that can be stored in shared memory. For each plpgsql function (or procedure), the whole content is split into chunks of 30 statements. If needed, multiple chunks can be used to store the whole content of a single function. A single chunk is 1704 bytes. The default value for this GUC is 15000, which should be enough for big projects containing hundred of thousands of statements in plpgsql, and will consume about 24MB of memory. If your project doesn't require that much number of chunks, you can set this parameter to a smaller number in order to decrease the memory usage. The minimum value is 50 (which should consume about 83kB of memory), and the maximum value is 100000 (which should consume about 163MB of memory). Changing this parameter requires a PostgreSQL restart.

The profiler will also retrieve the query identifier for each instruction that contains an expression or optimizable statement. Note that this requires pg_stat_statements, or another similar third-party extension), to be installed. There are some limitations to the query identifier retrieval:

  • if a plpgsql expression contains underlying statements, only the top level query identifier will be retrieved
  • the profiler doesn't compute query identifier by itself but relies on external extension, such as pg_stat_statements, for that. It means that depending on the external extension behavior, you may not be able to see a query identifier for some statements. That's for instance the case with DDL statements, as pg_stat_statements doesn't expose the query identifier for such queries.
  • a query identifier is retrieved only for instructions containing expressions. This means that plpgsql_profiler_function_tb() function can report less query identifier than instructions on a single line.

Attention: A update of shared profiles can decrease performance on servers under higher load.

The profile can be displayed by function plpgsql_profiler_function_tb:

postgres=# select lineno, avg_time, source from plpgsql_profiler_function_tb('fx(int)');
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹 lineno 鈹 avg_time 鈹                              source                               鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺
鈹      1 鈹          鈹                                                                   鈹
鈹      2 鈹          鈹 declare result int = 0;                                           鈹
鈹      3 鈹    0.075 鈹 begin                                                             鈹
鈹      4 鈹    0.202 鈹   for i in 1..$1 loop                                             鈹
鈹      5 鈹    0.005 鈹     select result + i into result; select result + i into result; 鈹
鈹      6 鈹          鈹   end loop;                                                       鈹
鈹      7 鈹        0 鈹   return result;                                                  鈹
鈹      8 鈹          鈹 end;                                                              鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(9 rows)

The profile per statements (not per line) can be displayed by function plpgsql_profiler_function_statements_tb:

        CREATE OR REPLACE FUNCTION public.fx1(a integer)
         RETURNS integer
         LANGUAGE plpgsql
1       AS $function$
2       begin
3         if a > 10 then
4           raise notice 'ahoj';
5           return -1;
6         else
7           raise notice 'nazdar';
8           return 1;
9         end if;
10      end;
11      $function$

postgres=# select stmtid, parent_stmtid, parent_note, lineno, exec_stmts, stmtname
             from plpgsql_profiler_function_statements_tb('fx1');
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹 stmtid 鈹 parent_stmtid 鈹 parent_note 鈹 lineno 鈹 exec_stmts 鈹    stmtname     鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹      0 鈹             鈭 鈹 鈭           鈹      2 鈹          0 鈹 statement block 鈹
鈹      1 鈹             0 鈹 body        鈹      3 鈹          0 鈹 IF              鈹
鈹      2 鈹             1 鈹 then body   鈹      4 鈹          0 鈹 RAISE           鈹
鈹      3 鈹             1 鈹 then body   鈹      5 鈹          0 鈹 RETURN          鈹
鈹      4 鈹             1 鈹 else body   鈹      7 鈹          0 鈹 RAISE           鈹
鈹      5 鈹             1 鈹 else body   鈹      8 鈹          0 鈹 RETURN          鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(6 rows)

All stored profiles can be displayed by calling function plpgsql_profiler_functions_all:

postgres=# select * from plpgsql_profiler_functions_all();
鈹屸攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
鈹        funcoid        鈹 exec_count 鈹 total_time 鈹 avg_time 鈹 stddev_time 鈹 min_time 鈹 max_time 鈹
鈺炩晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暘鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺晲鈺愨晲鈺愨晲鈺愨晲鈺愨晲鈺愨暋
鈹 fxx(double precision) 鈹          1 鈹       0.01 鈹     0.01 鈹        0.00 鈹     0.01 鈹     0.01 鈹
鈹斺攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹粹攢鈹鈹鈹鈹鈹鈹鈹鈹鈹鈹
(1 row)

There are two functions for cleaning stored profiles: plpgsql_profiler_reset_all() and plpgsql_profiler_reset(regprocedure).

Coverage metrics

plpgsql_check provides two functions:

  • plpgsql_coverage_statements(name)
  • plpgsql_coverage_branches(name)

Note

There is another very good PLpgSQL profiler - https://bitbucket.org/openscg/plprofiler

My extension is designed to be simple for use and practical. Nothing more or less.

plprofiler is more complex. It build call graphs and from this graph it can creates flame graph of execution times.

Both extensions can be used together with buildin PostgreSQL's feature - tracking functions.

set track_functions to 'pl';
...
select * from pg_stat_user_functions;

Tracer

plpgsql_check provides a tracing possibility - in this mode you can see notices on start or end functions (terse and default verbosity) and start or end statements (verbose verbosity). For default and verbose verbosity the content of function arguments is displayed. The content of related variables are displayed when verbosity is verbose.

postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #0 ->> start of inline_code_block (Oid=0)
NOTICE:  #2   ->> start of function fx(integer,integer,date,text) (Oid=16405)
NOTICE:  #2        call by inline_code_block line 1 at PERFORM
NOTICE:  #2       "a" => '10', "b" => null, "c" => '2020-08-03', "d" => 'st臎hule'
NOTICE:  #4     ->> start of function fx(integer) (Oid=16404)
NOTICE:  #4          call by fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:  #4         "a" => '10'
NOTICE:  #4     <<- end of function fx (elapsed time=0.098 ms)
NOTICE:  #2   <<- end of function fx (elapsed time=0.399 ms)
NOTICE:  #0 <<- end of block (elapsed time=0.754 ms)

The number after # is a execution frame counter (this number is related to deep of error context stack). It allows to pair start end and of function.

Tracing is enabled by setting plpgsql_check.tracer to on. Attention - enabling this behaviour has significant negative impact on performance (unlike the profiler). You can set a level for output used by tracer plpgsql_check.tracer_errlevel (default is notice). The output content is limited by length specified by plpgsql_check.tracer_variable_max_length configuration variable.

In terse verbose mode the output is reduced:

postgres=# set plpgsql_check.tracer_verbosity TO terse;
SET
postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #0 start of inline code block (oid=0)
NOTICE:  #2 start of fx (oid=16405)
NOTICE:  #4 start of fx (oid=16404)
NOTICE:  #4 end of fx
NOTICE:  #2 end of fx
NOTICE:  #0 end of inline code block

In verbose mode the output is extended about statement details:

postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #0            ->> start of block inline_code_block (oid=0)
NOTICE:  #0.1       1  --> start of PERFORM
NOTICE:  #2              ->> start of function fx(integer,integer,date,text) (oid=16405)
NOTICE:  #2                   call by inline_code_block line 1 at PERFORM
NOTICE:  #2                  "a" => '10', "b" => null, "c" => '2020-08-04', "d" => 'st臎hule'
NOTICE:  #2.1       1    --> start of PERFORM
NOTICE:  #2.1                "a" => '10'
NOTICE:  #4                ->> start of function fx(integer) (oid=16404)
NOTICE:  #4                     call by fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:  #4                    "a" => '10'
NOTICE:  #4.1       6      --> start of assignment
NOTICE:  #4.1                  "a" => '10', "b" => '20'
NOTICE:  #4.1              <-- end of assignment (elapsed time=0.076 ms)
NOTICE:  #4.1                  "res" => '130'
NOTICE:  #4.2       7      --> start of RETURN
NOTICE:  #4.2                  "res" => '130'
NOTICE:  #4.2              <-- end of RETURN (elapsed time=0.054 ms)
NOTICE:  #4                <<- end of function fx (elapsed time=0.373 ms)
NOTICE:  #2.1            <-- end of PERFORM (elapsed time=0.589 ms)
NOTICE:  #2              <<- end of function fx (elapsed time=0.727 ms)
NOTICE:  #0.1          <-- end of PERFORM (elapsed time=1.147 ms)
NOTICE:  #0            <<- end of block (elapsed time=1.286 ms)

Special feature of tracer is tracing of ASSERT statement when plpgsql_check.trace_assert is on. When plpgsql_check.trace_assert_verbosity is DEFAULT, then all function's or procedure's variables are displayed when assert expression is false. When this configuration is VERBOSE then all variables from all plpgsql frames are displayed. This behaviour is independent on plpgsql.check_asserts value. It can be used, although the assertions are disabled in plpgsql runtime.

postgres=# set plpgsql_check.tracer to off;
postgres=# set plpgsql_check.trace_assert_verbosity TO verbose;

postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false
NOTICE:   "a" => '10', "res" => null, "b" => '20'
NOTICE:  #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:   "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'st臎hule'
NOTICE:  #0 PL/pgSQL function inline_code_block line 1 at PERFORM
ERROR:  assertion failed
CONTEXT:  PL/pgSQL function fx(integer) line 12 at ASSERT
SQL statement "SELECT fx(a)"
PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
SQL statement "SELECT fx(10,null, 'now', e'st臎hule')"
PL/pgSQL function inline_code_block line 1 at PERFORM

postgres=# set plpgsql.check_asserts to off;
SET
postgres=# do $$ begin perform fx(10,null, 'now', e'st臎hule'); end; $$;
NOTICE:  #4 PLpgSQL assert expression (false) on line 12 of fx(integer) is false
NOTICE:   "a" => '10', "res" => null, "b" => '20'
NOTICE:  #2 PL/pgSQL function fx(integer,integer,date,text) line 1 at PERFORM
NOTICE:   "a" => '10', "b" => null, "c" => '2020-08-05', "d" => 'st臎hule'
NOTICE:  #0 PL/pgSQL function inline_code_block line 1 at PERFORM
DO

Attention - SECURITY

Tracer prints content of variables or function arguments. For security definer function, this content can hold security sensitive data. This is reason why tracer is disabled by default and should be enabled only with super user rights plpgsql_check.enable_tracer.

Pragma

You can configure plpgsql_check behave inside checked function with "pragma" function. This is a analogy of PL/SQL or ADA language of PRAGMA feature. PLpgSQL doesn't support PRAGMA, but plpgsql_check detects function named plpgsql_check_pragma and get options from parameters of this function. These plpgsql_check options are valid to end of group of statements.

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
BEGIN
  ...
  -- for following statements disable check
  PERFORM plpgsql_check_pragma('disable:check');
  ...
  -- enable check again
  PERFORM plpgsql_check_pragma('enable:check');
  ...
END;
$$ LANGUAGE plpgsql;

The function plpgsql_check_pragma is immutable function that returns one. It is defined by plpgsql_check extension. You can declare alternative plpgsql_check_pragma function like:

CREATE OR REPLACE FUNCTION plpgsql_check_pragma(VARIADIC args[])
RETURNS int AS $$
SELECT 1
$$ LANGUAGE sql IMMUTABLE;

Using pragma function in declaration part of top block sets options on function level too.

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
DECLARE
  aux int := plpgsql_check_pragma('disable:extra_warnings');
  ...

Shorter syntax for pragma is supported too:

CREATE OR REPLACE FUNCTION test()
RETURNS void AS $$
DECLARE r record;
BEGIN
  PERFORM 'PRAGMA:TYPE:r (a int, b int)';
  PERFORM 'PRAGMA:TABLE: x (like pg_class)';
  ...

Supported pragmas

echo:str - print string (for testing)

status:check,status:tracer, status:other_warnings, status:performance_warnings, status:extra_warnings,status:security_warnings

enable:check,enable:tracer, enable:other_warnings, enable:performance_warnings, enable:extra_warnings,enable:security_warnings

disable:check,disable:tracer, disable:other_warnings, disable:performance_warnings, disable:extra_warnings,disable:security_warnings

type:varname typename or type:varname (fieldname type, ...) - set type to variable of record type

table: name (column_name type, ...) or table: name (like tablename) - create ephereal table

Pragmas enable:tracer and disable:tracerare active for Postgres 12 and higher

Compilation

You need a development environment for PostgreSQL extensions:

make clean
make install

result:

[pavel@localhost plpgsql_check]$ make USE_PGXS=1 clean
rm -f plpgsql_check.so   libplpgsql_check.a  libplpgsql_check.pc
rm -f plpgsql_check.o
rm -rf results/ regression.diffs regression.out tmp_check/ log/
[pavel@localhost plpgsql_check]$ make USE_PGXS=1 all
clang -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fpic -I/usr/local/pgsql/lib/pgxs/src/makefiles/../../src/pl/plpgsql/src -I. -I./ -I/usr/local/pgsql/include/server -I/usr/local/pgsql/include/internal -D_GNU_SOURCE   -c -o plpgsql_check.o plpgsql_check.c
clang -O2 -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -fpic -I/usr/local/pgsql/lib/pgxs/src/makefiles/../../src/pl/plpgsql/src -shared -o plpgsql_check.so plpgsql_check.o -L/usr/local/pgsql/lib -Wl,--as-needed -Wl,-rpath,'/usr/local/pgsql/lib',--enable-new-dtags  
[pavel@localhost plpgsql_check]$ su root
Password: *******
[root@localhost plpgsql_check]# make USE_PGXS=1 install
/usr/bin/mkdir -p '/usr/local/pgsql/lib'
/usr/bin/mkdir -p '/usr/local/pgsql/share/extension'
/usr/bin/mkdir -p '/usr/local/pgsql/share/extension'
/usr/bin/install -c -m 755  plpgsql_check.so '/usr/local/pgsql/lib/plpgsql_check.so'
/usr/bin/install -c -m 644 plpgsql_check.control '/usr/local/pgsql/share/extension/'
/usr/bin/install -c -m 644 plpgsql_check--0.9.sql '/usr/local/pgsql/share/extension/'
[root@localhost plpgsql_check]# exit
[pavel@localhost plpgsql_check]$ make USE_PGXS=1 installcheck
/usr/local/pgsql/lib/pgxs/src/makefiles/../../src/test/regress/pg_regress --inputdir=./ --psqldir='/usr/local/pgsql/bin'    --dbname=pl_regression --load-language=plpgsql --dbname=contrib_regression plpgsql_check_passive plpgsql_check_active plpgsql_check_active-9.5
(using postmaster on Unix socket, default port)
============== dropping database "contrib_regression" ==============
DROP DATABASE
============== creating database "contrib_regression" ==============
CREATE DATABASE
ALTER DATABASE
============== installing plpgsql                     ==============
CREATE LANGUAGE
============== running regression test queries        ==============
test plpgsql_check_passive    ... ok
test plpgsql_check_active     ... ok
test plpgsql_check_active-9.5 ... ok

=====================
 All 3 tests passed. 
=====================

Compilation on Ubuntu

Sometimes successful compilation can require libicu-dev package (PostgreSQL 10 and higher - when pg was compiled with ICU support)

sudo apt install libicu-dev

Compilation plpgsql_check on Windows

You can check precompiled dll libraries http://okbob.blogspot.cz/2015/02/plpgsqlcheck-is-available-for-microsoft.html

or compile by self:

  1. Download and install PostgreSQL for Win32 from http://www.enterprisedb.com
  2. Download and install Microsoft Visual C++ Express
  3. Lern tutorial http://blog.2ndquadrant.com/compiling-postgresql-extensions-visual-studio-windows
  4. Build plpgsql_check.dll
  5. Install plugin
  6. copy plpgsql_check.dll to PostgreSQL\14\lib
  7. copy plpgsql_check.control and plpgsql_check--2.1.sql to PostgreSQL\14\share\extension

Checked on

  • gcc on Linux (against all supported PostgreSQL)
  • clang 3.4 on Linux (against PostgreSQL 10)
  • for success regress tests the PostgreSQL 10 or higher is required

Compilation against PostgreSQL 10 requires libICU!

Licence

Copyright (c) Pavel Stehule (pavel.stehule@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Note

If you like it, send a postcard to address

Pavel Stehule
Skalice 12
256 01 Benesov u Prahy
Czech Republic

I invite any questions, comments, bug reports, patches on mail address pavel.stehule@gmail.com


Author: okbob
Source Code: https://github.com/okbob/plpgsql_check
License: View license

#postgresql